From 8bfe2ec71e4800b798d7c0d7b3224ff2530a1c79 Mon Sep 17 00:00:00 2001 From: Daniel Somerfield Date: Tue, 21 Nov 2023 14:02:26 -0800 Subject: [PATCH 001/433] Fix all_fruits_types_in_basket to fail if all fruit kinds are not included --- exercises/11_hashmaps/hashmaps2.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index a5925690..ab918cd8 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -18,7 +18,7 @@ use std::collections::HashMap; -#[derive(Hash, PartialEq, Eq)] +#[derive(Hash, PartialEq, Eq, Debug)] enum Fruit { Apple, Banana, @@ -27,6 +27,14 @@ enum Fruit { Pineapple, } +const FRUIT_KINDS: [Fruit; 5] = [ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, +]; + fn fruit_basket(basket: &mut HashMap) { let fruit_kinds = vec![ Fruit::Apple, @@ -81,12 +89,15 @@ mod tests { let count = basket.values().sum::(); assert!(count > 11); } - + #[test] fn all_fruit_types_in_basket() { let mut basket = get_fruit_basket(); fruit_basket(&mut basket); - for amount in basket.values() { + for fruit_kind in FRUIT_KINDS { + let amount = basket + .get(&fruit_kind) + .expect(format!("Fruit kind {:?} was not found in basket", fruit_kind).as_str()); assert_ne!(amount, &0); } } From 62afbb034f74dc170347d3eff5131e780930d639 Mon Sep 17 00:00:00 2001 From: Daniel Somerfield Date: Wed, 27 Mar 2024 20:37:19 -0700 Subject: [PATCH 002/433] Move test array to be in test module as vec --- exercises/11_hashmaps/hashmaps2.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index ab918cd8..a860d7b2 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -27,14 +27,6 @@ enum Fruit { Pineapple, } -const FRUIT_KINDS: [Fruit; 5] = [ - Fruit::Apple, - Fruit::Banana, - Fruit::Mango, - Fruit::Lychee, - Fruit::Pineapple, -]; - fn fruit_basket(basket: &mut HashMap) { let fruit_kinds = vec![ Fruit::Apple, @@ -92,9 +84,17 @@ mod tests { #[test] fn all_fruit_types_in_basket() { + let fruit_kinds = vec![ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, + ]; + let mut basket = get_fruit_basket(); fruit_basket(&mut basket); - for fruit_kind in FRUIT_KINDS { + for fruit_kind in fruit_kinds { let amount = basket .get(&fruit_kind) .expect(format!("Fruit kind {:?} was not found in basket", fruit_kind).as_str()); From e5efc68a9101d7d7e38263c8a6ee44dda991fc6a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 17:34:48 +0100 Subject: [PATCH 003/433] Done macro --- Cargo.lock | 8 ++++ Cargo.toml | 21 +++++--- rustlings-macros/Cargo.toml | 12 +++++ rustlings-macros/src/lib.rs | 95 +++++++++++++++++++++++++++++++++++++ src/main.rs | 23 +++++++++ 5 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 rustlings-macros/Cargo.toml create mode 100644 rustlings-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f4853d0c..e432072d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,6 +574,7 @@ dependencies = [ "indicatif", "notify-debouncer-mini", "predicates", + "rustlings-macros", "serde", "serde_json", "shlex", @@ -582,6 +583,13 @@ dependencies = [ "winnow", ] +[[package]] +name = "rustlings-macros" +version = "5.6.1" +dependencies = [ + "quote", +] + [[package]] name = "ryu" version = "1.0.17" diff --git a/Cargo.toml b/Cargo.toml index 2d152cfc..e08be8bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,30 @@ -[package] -name = "rustlings" -description = "Small exercises to get you used to reading and writing Rust code!" +[workspace] +resolver = "2" + +[workspace.package] version = "5.6.1" authors = [ "Liv ", "Carol (Nichols || Goulding) ", ] +license = "MIT" edition = "2021" +[package] +name = "rustlings" +description = "Small exercises to get you used to reading and writing Rust code!" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + [dependencies] anyhow = "1.0.81" clap = { version = "4.5.4", features = ["derive"] } console = "0.15.8" indicatif = "0.17.8" notify-debouncer-mini = "0.4.1" +rustlings-macros = { path = "rustlings-macros" } serde_json = "1.0.115" serde = { version = "1.0.197", features = ["derive"] } shlex = "1.3.0" @@ -21,10 +32,6 @@ toml_edit = { version = "0.22.9", default-features = false, features = ["parse", which = "6.0.1" winnow = "0.6.5" -[[bin]] -name = "rustlings" -path = "src/main.rs" - [dev-dependencies] assert_cmd = "2.0.14" glob = "0.3.0" diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml new file mode 100644 index 00000000..0114c8f0 --- /dev/null +++ b/rustlings-macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rustlings-macros" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.35" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs new file mode 100644 index 00000000..dd1a5881 --- /dev/null +++ b/rustlings-macros/src/lib.rs @@ -0,0 +1,95 @@ +use proc_macro::TokenStream; +use quote::quote; +use std::{fs::read_dir, panic, path::PathBuf}; + +fn path_to_string(path: PathBuf) -> String { + path.into_os_string() + .into_string() + .unwrap_or_else(|original| { + panic!("The path {} is invalid UTF8", original.to_string_lossy()); + }) +} + +#[proc_macro] +pub fn include_files(_: TokenStream) -> TokenStream { + let mut files = Vec::with_capacity(8); + let mut dirs = Vec::with_capacity(128); + + for entry in read_dir("exercises").expect("Failed to open the exercises directory") { + let entry = entry.expect("Failed to read the exercises directory"); + + if entry.file_type().unwrap().is_file() { + let path = entry.path(); + if path.file_name().unwrap() != "README.md" { + files.push(path_to_string(path)); + } + + continue; + } + + let dir_path = entry.path(); + let dir_files = read_dir(&dir_path).unwrap_or_else(|e| { + panic!("Failed to open the directory {}: {e}", dir_path.display()); + }); + let dir_path = path_to_string(dir_path); + let dir_files = dir_files.filter_map(|entry| { + let entry = entry.unwrap_or_else(|e| { + panic!("Failed to read the directory {dir_path}: {e}"); + }); + let path = entry.path(); + + if !entry.file_type().unwrap().is_file() { + panic!("Found {} but expected only files", path.display()); + } + + if path.file_name().unwrap() == "README.md" { + return None; + } + + if path.extension() != Some("rs".as_ref()) { + panic!( + "Found {} but expected only README.md and .rs files", + path.display(), + ); + } + + Some(path_to_string(path)) + }); + + dirs.push(quote! { + EmbeddedFlatDir { + path: #dir_path, + readme: EmbeddedFile { + path: concat!(#dir_path, "/README.md"), + content: ::std::include_bytes!(concat!("../", #dir_path, "/README.md")), + }, + content: vec![ + #(EmbeddedFile { + path: #dir_files, + content: ::std::include_bytes!(concat!("../", #dir_files)), + }),* + ], + } + }); + } + + quote! { + EmbeddedFiles { + info_toml_content: ::std::include_str!("../info.toml"), + exercises_dir: ExercisesDir { + readme: EmbeddedFile { + path: "exercises/README.md", + content: ::std::include_bytes!("../exercises/README.md"), + }, + files: vec![#( + EmbeddedFile { + path: #files, + content: ::std::include_bytes!(concat!("../", #files)), + } + ),*], + dirs: vec![#(#dirs),*], + }, + } + } + .into() +} diff --git a/src/main.rs b/src/main.rs index 8f73dbba..fed8c117 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,28 @@ mod project; mod run; mod verify; +struct EmbeddedFile { + path: &'static str, + content: &'static [u8], +} + +struct EmbeddedFlatDir { + path: &'static str, + readme: EmbeddedFile, + content: Vec, +} + +struct ExercisesDir { + readme: EmbeddedFile, + files: Vec, + dirs: Vec, +} + +struct EmbeddedFiles { + info_toml_content: &'static str, + exercises_dir: ExercisesDir, +} + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -87,6 +109,7 @@ enum Subcommands { } fn main() -> Result<()> { + let embedded_files = rustlings_macros::include_files!(); let args = Args::parse(); if args.command.is_none() { From dd025391f2f3a4cb0a45e28163b01538b4b525cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 17:52:51 +0100 Subject: [PATCH 004/433] Make everything static --- rustlings-macros/src/lib.rs | 6 +++--- src/main.rs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index dd1a5881..d8cd05ce 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -63,7 +63,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { path: concat!(#dir_path, "/README.md"), content: ::std::include_bytes!(concat!("../", #dir_path, "/README.md")), }, - content: vec![ + content: &[ #(EmbeddedFile { path: #dir_files, content: ::std::include_bytes!(concat!("../", #dir_files)), @@ -81,13 +81,13 @@ pub fn include_files(_: TokenStream) -> TokenStream { path: "exercises/README.md", content: ::std::include_bytes!("../exercises/README.md"), }, - files: vec![#( + files: &[#( EmbeddedFile { path: #files, content: ::std::include_bytes!(concat!("../", #files)), } ),*], - dirs: vec![#(#dirs),*], + dirs: &[#(#dirs),*], }, } } diff --git a/src/main.rs b/src/main.rs index fed8c117..7822d122 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,13 +35,13 @@ struct EmbeddedFile { struct EmbeddedFlatDir { path: &'static str, readme: EmbeddedFile, - content: Vec, + content: &'static [EmbeddedFile], } struct ExercisesDir { readme: EmbeddedFile, - files: Vec, - dirs: Vec, + files: &'static [EmbeddedFile], + dirs: &'static [EmbeddedFlatDir], } struct EmbeddedFiles { @@ -49,6 +49,8 @@ struct EmbeddedFiles { exercises_dir: ExercisesDir, } +static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -109,7 +111,6 @@ enum Subcommands { } fn main() -> Result<()> { - let embedded_files = rustlings_macros::include_files!(); let args = Args::parse(); if args.command.is_none() { From 39bdd086a775d87115691b830f65e2a438874fec Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 18:18:20 +0100 Subject: [PATCH 005/433] Use concat explicitly from std --- rustlings-macros/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index d8cd05ce..598b5c35 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -60,13 +60,13 @@ pub fn include_files(_: TokenStream) -> TokenStream { EmbeddedFlatDir { path: #dir_path, readme: EmbeddedFile { - path: concat!(#dir_path, "/README.md"), - content: ::std::include_bytes!(concat!("../", #dir_path, "/README.md")), + path: ::std::concat!(#dir_path, "/README.md"), + content: ::std::include_bytes!(::std::concat!("../", #dir_path, "/README.md")), }, content: &[ #(EmbeddedFile { path: #dir_files, - content: ::std::include_bytes!(concat!("../", #dir_files)), + content: ::std::include_bytes!(::std::concat!("../", #dir_files)), }),* ], } @@ -84,7 +84,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { files: &[#( EmbeddedFile { path: #files, - content: ::std::include_bytes!(concat!("../", #files)), + content: ::std::include_bytes!(::std::concat!("../", #files)), } ),*], dirs: &[#(#dirs),*], From d5ed749e9fde03212fd6fe5d60e2ddfe9b2429c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 21:06:36 +0100 Subject: [PATCH 006/433] Add embedded.rs --- src/embedded.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 25 +----------- 2 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 src/embedded.rs diff --git a/src/embedded.rs b/src/embedded.rs new file mode 100644 index 00000000..8f6c14e7 --- /dev/null +++ b/src/embedded.rs @@ -0,0 +1,101 @@ +use std::{ + fs::{create_dir, File, OpenOptions}, + io::{self, Write}, + path::Path, +}; + +pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); + +#[derive(Clone, Copy)] +pub enum WriteStrategy { + IfNotExists, + Overwrite, +} + +impl WriteStrategy { + fn open>(self, path: P) -> io::Result { + match self { + Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), + Self::Overwrite => OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path), + } + } +} + +struct EmbeddedFile { + path: &'static str, + content: &'static [u8], +} + +impl EmbeddedFile { + fn write_to_disk(&self, strategy: WriteStrategy) -> io::Result<()> { + strategy.open(self.path)?.write_all(self.content) + } +} + +struct EmbeddedFlatDir { + path: &'static str, + readme: EmbeddedFile, + content: &'static [EmbeddedFile], +} + +impl EmbeddedFlatDir { + fn init_on_disk(&self) -> io::Result<()> { + let path = Path::new(self.path); + + if let Err(e) = create_dir(path) { + if !path.is_dir() { + return Err(e); + } + } + + self.readme.write_to_disk(WriteStrategy::Overwrite) + } +} + +struct ExercisesDir { + readme: EmbeddedFile, + files: &'static [EmbeddedFile], + dirs: &'static [EmbeddedFlatDir], +} + +pub struct EmbeddedFiles { + info_toml_content: &'static str, + exercises_dir: ExercisesDir, +} + +impl EmbeddedFiles { + pub fn init_exercises_dir(&self) -> io::Result<()> { + create_dir("exercises")?; + self.exercises_dir + .readme + .write_to_disk(WriteStrategy::Overwrite) + } + + pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { + if let Some(file) = self + .exercises_dir + .files + .iter() + .find(|file| file.path == path.as_os_str()) + { + return file.write_to_disk(strategy); + } + + for dir in self.exercises_dir.dirs { + if let Some(file) = dir + .content + .iter() + .find(|file| file.path == path.as_os_str()) + { + dir.init_on_disk()?; + return file.write_to_disk(strategy); + } + } + + Err(io::Error::from(io::ErrorKind::NotFound)) + } +} diff --git a/src/main.rs b/src/main.rs index 7822d122..1e0aa668 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,35 +22,12 @@ use std::time::Duration; #[macro_use] mod ui; +mod embedded; mod exercise; mod project; mod run; mod verify; -struct EmbeddedFile { - path: &'static str, - content: &'static [u8], -} - -struct EmbeddedFlatDir { - path: &'static str, - readme: EmbeddedFile, - content: &'static [EmbeddedFile], -} - -struct ExercisesDir { - readme: EmbeddedFile, - files: &'static [EmbeddedFile], - dirs: &'static [EmbeddedFlatDir], -} - -struct EmbeddedFiles { - info_toml_content: &'static str, - exercises_dir: ExercisesDir, -} - -static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); - /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] From 5b4103bbac180fcb1de747214647811a3622b476 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 21:10:31 +0100 Subject: [PATCH 007/433] Remove unneeded ./ from relative paths --- src/exercise.rs | 4 ++-- src/main.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 19f528a8..16e4a41c 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -13,7 +13,7 @@ const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; const CONTEXT: usize = 2; -const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml"; +const CLIPPY_CARGO_TOML_PATH: &str = "exercises/22_clippy/Cargo.toml"; // Checks if the line contains the "I AM NOT DONE" comment. fn contains_not_done_comment(input: &str) -> bool { @@ -36,7 +36,7 @@ fn temp_file() -> String { .filter(|c| c.is_alphanumeric()) .collect(); - format!("./temp_{}_{thread_id}", process::id()) + format!("temp_{}_{thread_id}", process::id()) } // The mode of the exercise. diff --git a/src/main.rs b/src/main.rs index 1e0aa668..90d0109c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -342,7 +342,7 @@ fn watch( let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; debouncer .watcher() - .watch(Path::new("./exercises"), RecursiveMode::Recursive)?; + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; clear_screen(); From 3ff9b0cd2a92a531e8c7a9f8a0f86b9fac04d252 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 28 Mar 2024 22:11:16 +0100 Subject: [PATCH 008/433] POC done --- src/embedded.rs | 23 ++++++++++++++--- src/exercise.rs | 2 +- src/main.rs | 67 ++++++++++++++++++++++++++++--------------------- src/project.rs | 20 +++++++-------- src/run.rs | 16 +++--------- 5 files changed, 74 insertions(+), 54 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 8f6c14e7..25dbe641 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -52,7 +52,9 @@ impl EmbeddedFlatDir { } } - self.readme.write_to_disk(WriteStrategy::Overwrite) + self.readme.write_to_disk(WriteStrategy::Overwrite)?; + + Ok(()) } } @@ -63,16 +65,31 @@ struct ExercisesDir { } pub struct EmbeddedFiles { - info_toml_content: &'static str, + pub info_toml_content: &'static str, exercises_dir: ExercisesDir, } impl EmbeddedFiles { pub fn init_exercises_dir(&self) -> io::Result<()> { create_dir("exercises")?; + self.exercises_dir .readme - .write_to_disk(WriteStrategy::Overwrite) + .write_to_disk(WriteStrategy::IfNotExists)?; + + for file in self.exercises_dir.files { + file.write_to_disk(WriteStrategy::IfNotExists)?; + } + + for dir in self.exercises_dir.dirs { + dir.init_on_disk()?; + + for file in dir.content { + file.write_to_disk(WriteStrategy::IfNotExists)?; + } + } + + Ok(()) } pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { diff --git a/src/exercise.rs b/src/exercise.rs index 16e4a41c..7c2e5fde 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -36,7 +36,7 @@ fn temp_file() -> String { .filter(|c| c.is_alphanumeric()) .collect(); - format!("temp_{}_{thread_id}", process::id()) + format!("./temp_{}_{thread_id}", process::id()) } // The mode of the exercise. diff --git a/src/main.rs b/src/main.rs index 90d0109c..822cd1ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,14 @@ use crate::verify::verify; use anyhow::Result; use clap::{Parser, Subcommand}; use console::Emoji; +use embedded::EMBEDDED_FILES; use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; +use std::io::{self, prelude::*, stdin, stdout}; use std::path::Path; -use std::process::Command; +use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; @@ -54,7 +54,7 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// Reset a single exercise using "git stash -- " + /// Reset a single exercise Reset { /// The name of the exercise name: String, @@ -83,13 +83,45 @@ enum Subcommands { #[arg(short, long)] solved: bool, }, - /// Enable rust-analyzer for exercises - Lsp, } fn main() -> Result<()> { let args = Args::parse(); + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + .unwrap() + .exercises; + + if !Path::new("exercises").is_dir() { + let mut stdout = stdout().lock(); + write!( + stdout, + "The `exercises` directory wasn't found in the current directory. +Do you want to initialize Rustlings in the current directory (y/n)? " + )?; + stdout.flush()?; + let mut answer = String::new(); + stdin().read_line(&mut answer)?; + answer.make_ascii_lowercase(); + if answer.trim() != "y" { + exit(1); + } + + EMBEDDED_FILES.init_exercises_dir()?; + if let Err(e) = write_project_json(&exercises) { + writeln!( + stdout, + "Failed to write rust-project.json to disk for rust-analyzer: {e}" + )?; + } else { + writeln!(stdout, "Successfully generated rust-project.json")?; + writeln!( + stdout, + "rust-analyzer will now parse exercises, restart your language server or editor" + )?; + } + } + if args.command.is_none() { println!("\n{WELCOME}\n"); } @@ -101,18 +133,6 @@ fn main() -> Result<()> { std::process::exit(1); } - let info_file = fs::read_to_string("info.toml").unwrap_or_else(|e| { - match e.kind() { - io::ErrorKind::NotFound => println!( - "The program must be run from the rustlings directory\nTry `cd rustlings/`!", - ), - _ => println!("Failed to read the info.toml file: {e}"), - } - std::process::exit(1); - }); - let exercises = toml_edit::de::from_str::(&info_file) - .unwrap() - .exercises; let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { @@ -205,7 +225,7 @@ fn main() -> Result<()> { Subcommands::Reset { name } => { let exercise = find_exercise(&name, &exercises); - reset(exercise).unwrap_or_else(|_| std::process::exit(1)); + reset(exercise)?; } Subcommands::Hint { name } => { @@ -219,15 +239,6 @@ fn main() -> Result<()> { .unwrap_or_else(|_| std::process::exit(1)); } - Subcommands::Lsp => { - if let Err(e) = write_project_json(exercises) { - println!("Failed to write rust-project.json to disk for rust-analyzer: {e}"); - } else { - println!("Successfully generated rust-project.json"); - println!("rust-analyzer will now parse exercises, restart your language server or editor"); - } - } - Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Err(e) => { println!("Error: Could not watch your progress. Error message was {e:?}."); diff --git a/src/project.rs b/src/project.rs index 0f56de96..bb6caa58 100644 --- a/src/project.rs +++ b/src/project.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use serde::Serialize; use std::env; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use crate::exercise::Exercise; @@ -9,14 +9,14 @@ use crate::exercise::Exercise; /// Contains the structure of resulting rust-project.json file /// and functions to build the data required to create the file #[derive(Serialize)] -struct RustAnalyzerProject { +struct RustAnalyzerProject<'a> { sysroot_src: PathBuf, - crates: Vec, + crates: Vec>, } #[derive(Serialize)] -struct Crate { - root_module: PathBuf, +struct Crate<'a> { + root_module: &'a Path, edition: &'static str, // Not used, but required in the JSON file. deps: Vec<()>, @@ -25,12 +25,12 @@ struct Crate { cfg: [&'static str; 1], } -impl RustAnalyzerProject { - fn build(exercises: Vec) -> Result { +impl<'a> RustAnalyzerProject<'a> { + fn build(exercises: &'a [Exercise]) -> Result { let crates = exercises - .into_iter() + .iter() .map(|exercise| Crate { - root_module: exercise.path, + root_module: &exercise.path, edition: "2021", deps: Vec::new(), // This allows rust_analyzer to work inside `#[test]` blocks @@ -69,7 +69,7 @@ impl RustAnalyzerProject { } /// Write `rust-project.json` to disk. -pub fn write_project_json(exercises: Vec) -> Result<()> { +pub fn write_project_json(exercises: &[Exercise]) -> Result<()> { let content = RustAnalyzerProject::build(exercises)?; // Using the capacity 2^14 since the file length in bytes is higher than 2^13. diff --git a/src/run.rs b/src/run.rs index 6dd0388f..792bd8fd 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,6 +1,7 @@ -use std::process::Command; +use std::io; use std::time::Duration; +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, Mode}; use crate::verify::test; use indicatif::ProgressBar; @@ -19,17 +20,8 @@ pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { } // Resets the exercise by stashing the changes. -pub fn reset(exercise: &Exercise) -> Result<(), ()> { - let command = Command::new("git") - .arg("stash") - .arg("--") - .arg(&exercise.path) - .spawn(); - - match command { - Ok(_) => Ok(()), - Err(_) => Err(()), - } +pub fn reset(exercise: &Exercise) -> io::Result<()> { + EMBEDDED_FILES.write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) } // Invoke the rust compiler on the path of the given exercise From 3959570221c88bf7bebbc7427236ae5c90d9d630 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:25:21 +0100 Subject: [PATCH 009/433] Bump version to v6 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e432072d..d8e5b723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,7 +564,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "5.6.1" +version = "6.0.0" dependencies = [ "anyhow", "assert_cmd", @@ -585,7 +585,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "5.6.1" +version = "6.0.0" dependencies = [ "quote", ] diff --git a/Cargo.toml b/Cargo.toml index e08be8bf..690aecc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" [workspace.package] -version = "5.6.1" +version = "6.0.0" authors = [ "Liv ", "Carol (Nichols || Goulding) ", From 0f18d599e92189d5f3ba085dcb4c8c4da9c7f584 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:25:32 +0100 Subject: [PATCH 010/433] Add panic = "abort" --- Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 690aecc4..9224364b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,9 @@ winnow = "0.6.5" assert_cmd = "2.0.14" glob = "0.3.0" predicates = "3.1.0" + +[profile.release] +panic = "abort" + +[profile.dev] +panic = "abort" From 36a8e3ac0ee4f59ed587725e3257a79129a981e2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:29:41 +0100 Subject: [PATCH 011/433] Replace rust-project.json with Cargo.toml --- src/init.rs | 75 ++++++++++++++++++++++++++++++++++ src/main.rs | 108 +++++++++++++++++++++---------------------------- src/project.rs | 83 ------------------------------------- 3 files changed, 122 insertions(+), 144 deletions(-) create mode 100644 src/init.rs delete mode 100644 src/project.rs diff --git a/src/init.rs b/src/init.rs new file mode 100644 index 00000000..66535354 --- /dev/null +++ b/src/init.rs @@ -0,0 +1,75 @@ +use anyhow::{bail, Context, Result}; +use std::{ + env::set_current_dir, + fs::{create_dir, OpenOptions}, + io::{self, ErrorKind, Write}, + path::Path, +}; + +use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; + +fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { + let mut cargo_toml = Vec::with_capacity(1 << 13); + cargo_toml.extend_from_slice( + br#"[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false +"#, + ); + for exercise in exercises { + cargo_toml.extend_from_slice(b"\n[[bin]]\nname = \""); + cargo_toml.extend_from_slice(exercise.name.as_bytes()); + cargo_toml.extend_from_slice(b"\"\npath = \""); + cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); + cargo_toml.extend_from_slice(b"\"\n"); + } + OpenOptions::new() + .create_new(true) + .write(true) + .open("Cargo.toml")? + .write_all(&cargo_toml) +} + +fn create_vscode_dir() -> Result<()> { + create_dir(".vscode").context("Failed to create the directory `.vscode`")?; + let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; + OpenOptions::new() + .create_new(true) + .write(true) + .open(".vscode/extensions.json")? + .write_all(vs_code_extensions_json)?; + + Ok(()) +} + +pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> { + let rustlings_path = Path::new("rustlings"); + if let Err(e) = create_dir(rustlings_path) { + if e.kind() == ErrorKind::AlreadyExists { + bail!( + "A directory with the name `rustligs` already exists in the current directory. +You probably already initialized Rustlings. +Run `cd rustlings` +Then run `rustlings` again" + ); + } + return Err(e.into()); + } + + set_current_dir("rustlings") + .context("Failed to change the current directory to `rustlings`")?; + + EMBEDDED_FILES + .init_exercises_dir() + .context("Failed to initialize the `rustlings/exercises` directory")?; + + create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + + create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + + println!("\nDone initialization!\n"); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 822cd1ad..36c36b54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ use crate::exercise::{Exercise, ExerciseList}; -use crate::project::write_project_json; use crate::run::{reset, run}; use crate::verify::verify; -use anyhow::Result; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; use embedded::EMBEDDED_FILES; @@ -10,7 +9,7 @@ use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::io::{self, prelude::*, stdin, stdout}; +use std::io::{self, prelude::*}; use std::path::Path; use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -24,7 +23,7 @@ mod ui; mod embedded; mod exercise; -mod project; +mod init; mod run; mod verify; @@ -41,6 +40,8 @@ struct Args { #[derive(Subcommand)] enum Subcommands { + /// Initialize Rustlings + Init, /// Verify all exercises according to the recommended order Verify, /// Rerun `verify` when files were edited @@ -88,40 +89,6 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - .unwrap() - .exercises; - - if !Path::new("exercises").is_dir() { - let mut stdout = stdout().lock(); - write!( - stdout, - "The `exercises` directory wasn't found in the current directory. -Do you want to initialize Rustlings in the current directory (y/n)? " - )?; - stdout.flush()?; - let mut answer = String::new(); - stdin().read_line(&mut answer)?; - answer.make_ascii_lowercase(); - if answer.trim() != "y" { - exit(1); - } - - EMBEDDED_FILES.init_exercises_dir()?; - if let Err(e) = write_project_json(&exercises) { - writeln!( - stdout, - "Failed to write rust-project.json to disk for rust-analyzer: {e}" - )?; - } else { - writeln!(stdout, "Successfully generated rust-project.json")?; - writeln!( - stdout, - "rust-analyzer will now parse exercises, restart your language server or editor" - )?; - } - } - if args.command.is_none() { println!("\n{WELCOME}\n"); } @@ -133,14 +100,32 @@ Do you want to initialize Rustlings in the current directory (y/n)? " std::process::exit(1); } - let verbose = args.nocapture; + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + .unwrap() + .exercises; + if matches!(args.command, Some(Subcommands::Init)) { + init::init_rustlings(&exercises).context("Initialization failed")?; + println!("{DEFAULT_OUT}\n"); + return Ok(()); + } else if !Path::new("exercises").is_dir() { + println!( + "\nThe `exercises` directory wasn't found in the current directory. +If you are just starting with Rustlings and want to initialize it, +run the command `rustlings init`" + ); + exit(1); + } + + let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); std::process::exit(0); }); match command { + // `Init` is handled above. + Subcommands::Init => (), Subcommands::List { paths, names, @@ -421,9 +406,16 @@ fn watch( } } -const DEFAULT_OUT: &str = "Thanks for installing Rustlings! +const WELCOME: &str = r" welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/"; -Is this your first time? Don't worry, Rustlings was made for beginners! We are +const DEFAULT_OUT: &str = + "Is this your first time? Don't worry, Rustlings was made for beginners! We are going to teach you a lot of things about Rust, but before we can get started, here's a couple of notes about how Rustlings operates: @@ -446,8 +438,20 @@ started, here's a couple of notes about how Rustlings operates: 5. If you want to use `rust-analyzer` with exercises, which provides features like autocompletion, run the command `rustlings lsp`. -Got all that? Great! To get started, run `rustlings watch` in order to get the first -exercise. Make sure to have your editor open!"; +Got all that? Great! To get started, go into the new directory `rustlings` by +running `cd rustlings`. +Then, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the new `rustlings` directory!"; + +const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: + hint - prints the current exercise's hint + clear - clears the screen + quit - quits watch mode + ! - executes a command, like `!rustc --explain E0381` + help - displays this help message + +Watch mode automatically re-evaluates the current exercise +when you edit a file's contents."; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | @@ -475,21 +479,3 @@ You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; - -const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: - hint - prints the current exercise's hint - clear - clears the screen - quit - quits watch mode - ! - executes a command, like `!rustc --explain E0381` - help - displays this help message - -Watch mode automatically re-evaluates the current exercise -when you edit a file's contents."; diff --git a/src/project.rs b/src/project.rs deleted file mode 100644 index bb6caa58..00000000 --- a/src/project.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::{Context, Result}; -use serde::Serialize; -use std::env; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; - -use crate::exercise::Exercise; - -/// Contains the structure of resulting rust-project.json file -/// and functions to build the data required to create the file -#[derive(Serialize)] -struct RustAnalyzerProject<'a> { - sysroot_src: PathBuf, - crates: Vec>, -} - -#[derive(Serialize)] -struct Crate<'a> { - root_module: &'a Path, - edition: &'static str, - // Not used, but required in the JSON file. - deps: Vec<()>, - // Only `test` is used for all crates. - // Therefore, an array is used instead of a `Vec`. - cfg: [&'static str; 1], -} - -impl<'a> RustAnalyzerProject<'a> { - fn build(exercises: &'a [Exercise]) -> Result { - let crates = exercises - .iter() - .map(|exercise| Crate { - root_module: &exercise.path, - edition: "2021", - deps: Vec::new(), - // This allows rust_analyzer to work inside `#[test]` blocks - cfg: ["test"], - }) - .collect(); - - if let Some(path) = env::var_os("RUST_SRC_PATH") { - return Ok(Self { - sysroot_src: PathBuf::from(path), - crates, - }); - } - - let toolchain = Command::new("rustc") - .arg("--print") - .arg("sysroot") - .stderr(Stdio::inherit()) - .output() - .context("Failed to get the sysroot from `rustc`. Do you have `rustc` installed?")? - .stdout; - - let toolchain = - String::from_utf8(toolchain).context("The toolchain path is invalid UTF8")?; - let toolchain = toolchain.trim_end(); - println!("Determined toolchain: {toolchain}\n"); - - let mut sysroot_src = PathBuf::with_capacity(256); - sysroot_src.extend([toolchain, "lib", "rustlib", "src", "rust", "library"]); - - Ok(Self { - sysroot_src, - crates, - }) - } -} - -/// Write `rust-project.json` to disk. -pub fn write_project_json(exercises: &[Exercise]) -> Result<()> { - let content = RustAnalyzerProject::build(exercises)?; - - // Using the capacity 2^14 since the file length in bytes is higher than 2^13. - // The final length is not known exactly because it depends on the user's sysroot path, - // the current number of exercises etc. - let mut buf = Vec::with_capacity(1 << 14); - serde_json::to_writer(&mut buf, &content)?; - std::fs::write("rust-project.json", buf)?; - - Ok(()) -} From a561a0f7f0378ac98ee4f025f5023c320af794b8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:51:08 +0100 Subject: [PATCH 012/433] Avoid reinitialization by mistake --- src/init.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/init.rs b/src/init.rs index 66535354..e640c258 100644 --- a/src/init.rs +++ b/src/init.rs @@ -45,6 +45,16 @@ fn create_vscode_dir() -> Result<()> { } pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> { + if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { + bail!( + "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist +in the current directory. It looks like Rustlings was already initialized here. +Run `rustlings` for instructions on getting started with the exercises. + +If you didn't already initialize Rustlings, please initialize it in another directory." + ); + } + let rustlings_path = Path::new("rustlings"); if let Err(e) = create_dir(rustlings_path) { if e.kind() == ErrorKind::AlreadyExists { From 2b01811fe9344fa4afdef95fb934745176cab1b2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:51:22 +0100 Subject: [PATCH 013/433] Fix typo --- src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index e640c258..1ec84847 100644 --- a/src/init.rs +++ b/src/init.rs @@ -59,7 +59,7 @@ If you didn't already initialize Rustlings, please initialize it in another dire if let Err(e) = create_dir(rustlings_path) { if e.kind() == ErrorKind::AlreadyExists { bail!( - "A directory with the name `rustligs` already exists in the current directory. + "A directory with the name `rustlings` already exists in the current directory. You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again" From 8e3cc9d70c627ace4553e4fe62af3443e970e64f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 29 Mar 2024 01:52:05 +0100 Subject: [PATCH 014/433] Improve printed information --- src/init.rs | 2 -- src/main.rs | 17 ++++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/init.rs b/src/init.rs index 1ec84847..1edcb23c 100644 --- a/src/init.rs +++ b/src/init.rs @@ -79,7 +79,5 @@ Then run `rustlings` again" create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - println!("\nDone initialization!\n"); - Ok(()) } diff --git a/src/main.rs b/src/main.rs index 36c36b54..76b63736 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,13 +106,16 @@ fn main() -> Result<()> { if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; - println!("{DEFAULT_OUT}\n"); + println!( + "\nDone initialization!\n +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` for further instructions on getting started." + ); return Ok(()); } else if !Path::new("exercises").is_dir() { println!( "\nThe `exercises` directory wasn't found in the current directory. -If you are just starting with Rustlings and want to initialize it, -run the command `rustlings init`" +If you are just starting with Rustlings, run the command `rustlings init` to initialize it." ); exit(1); } @@ -435,13 +438,9 @@ started, here's a couple of notes about how Rustlings operates: 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, and sometimes, other learners do too so you can help each other out! -5. If you want to use `rust-analyzer` with exercises, which provides features like - autocompletion, run the command `rustlings lsp`. -Got all that? Great! To get started, go into the new directory `rustlings` by -running `cd rustlings`. -Then, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the new `rustlings` directory!"; +Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the `rustlings` directory!"; const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: hint - prints the current exercise's hint From fe7d775818021acee7d5ae40c5cf9fdac69b2122 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 18:52:49 +0100 Subject: [PATCH 015/433] Remove the installation scripts --- install.ps1 | 94 --------------------------- install.sh | 184 ---------------------------------------------------- 2 files changed, 278 deletions(-) delete mode 100644 install.ps1 delete mode 100755 install.sh diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 844b0134..00000000 --- a/install.ps1 +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env pwsh - -#Requires -Version 5 -param($path = "$home/rustlings") - -Write-Host "Let's get you set up with Rustlings!" - -Write-Host "Checking requirements..." -if (Get-Command git -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Git is installed" -} else { - Write-Host "WARNING: Git does not seem to be installed." - Write-Host "Please download Git using your package manager or over https://git-scm.com/!" - exit 1 -} - -if (Get-Command rustc -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Rust is installed" -} else { - Write-Host "WARNING: Rust does not seem to be installed." - Write-Host "Please download Rust using https://rustup.rs!" - exit 1 -} - -if (Get-Command cargo -ErrorAction SilentlyContinue) { - Write-Host "SUCCESS: Cargo is installed" -} else { - Write-Host "WARNING: Cargo does not seem to be installed." - Write-Host "Please download Rust and Cargo using https://rustup.rs!" - exit 1 -} - -# Function that compares two versions strings v1 and v2 given in arguments (e.g 1.31 and 1.33.0). -# Returns 1 if v1 > v2, 0 if v1 == v2, 2 if v1 < v2. -function vercomp($v1, $v2) { - if ($v1 -eq $v2) { - return 0 - } - - $v1 = $v1.Replace(".", "0") - $v2 = $v2.Replace(".", "0") - if ($v1.Length -gt $v2.Length) { - $v2 = $v2.PadRight($v1.Length, "0") - } else { - $v1 = $v1.PadRight($v2.Length, "0") - } - - if ($v1 -gt $v2) { - return 1 - } else { - return 2 - } -} - -$rustVersion = $(rustc --version).Split(" ")[1] -$minRustVersion = "1.70" -if ((vercomp $rustVersion $minRustVersion) -eq 2) { - Write-Host "WARNING: Rust version is too old: $rustVersion - needs at least $minRustVersion" - Write-Host "Please update Rust with 'rustup update'" - exit 1 -} else { - Write-Host "SUCCESS: Rust is up to date" -} - -Write-Host "Cloning Rustlings at $path" -git clone -q https://github.com/rust-lang/rustlings $path -if (!($LASTEXITCODE -eq 0)) { - exit 1 -} - -# UseBasicParsing is deprecated, pwsh 6 or above will automatically use it, -# but anyone running pwsh 5 will have to pass the argument. -$version = Invoke-WebRequest -UseBasicParsing https://api.github.com/repos/rust-lang/rustlings/releases/latest ` - | ConvertFrom-Json | Select-Object -ExpandProperty tag_name - -Write-Host "Checking out version $version..." -Set-Location $path -git checkout -q tags/$version - -Write-Host "Installing the 'rustlings' executable..." -cargo install --force --path . -if (!(Get-Command rustlings -ErrorAction SilentlyContinue)) { - Write-Host "WARNING: Please check that you have '~/.cargo/bin' in your PATH environment variable!" -} - -# Checking whether Clippy is installed. -# Due to a bug in Cargo, this must be done with Rustup: https://github.com/rust-lang/rustup/issues/1514 -$clippy = (rustup component list | Select-String "clippy" | Select-String "installed") | Out-String -if (!$clippy) { - Write-Host "Installing the 'cargo-clippy' executable..." - rustup component add clippy -} - -Write-Host "All done! Navigate to $path and run 'rustlings' to get started!" diff --git a/install.sh b/install.sh deleted file mode 100755 index fdbe8d43..00000000 --- a/install.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -echo -e "\nLet's get you set up with Rustlings!" - -echo "Checking requirements..." -if [ -x "$(command -v git)" ] -then - echo "SUCCESS: Git is installed" -else - echo "ERROR: Git does not seem to be installed." - echo "Please download Git using your package manager or over https://git-scm.com/!" - exit 1 -fi - -if [ -x "$(command -v cc)" ] -then - echo "SUCCESS: cc is installed" -else - echo "ERROR: cc does not seem to be installed." - echo "Please download (g)cc using your package manager." - echo "OSX: xcode-select --install" - echo "Deb: sudo apt install gcc" - echo "Yum: sudo yum -y install gcc" - exit 1 -fi - -if [ -x "$(command -v rustup)" ] -then - echo "SUCCESS: rustup is installed" -else - echo "ERROR: rustup does not seem to be installed." - echo "Please download rustup using https://rustup.rs!" - exit 1 -fi - -if [ -x "$(command -v rustc)" ] -then - echo "SUCCESS: Rust is installed" -else - echo "ERROR: Rust does not seem to be installed." - echo "Please download Rust using rustup!" - exit 1 -fi - -if [ -x "$(command -v cargo)" ] -then - echo "SUCCESS: Cargo is installed" -else - echo "ERROR: Cargo does not seem to be installed." - echo "Please download Rust and Cargo using rustup!" - exit 1 -fi - -# Look up python installations, starting with 3 with a fallback of 2 -if [ -x "$(command -v python3)" ] -then - PY="$(command -v python3)" -elif [ -x "$(command -v python)" ] -then - PY="$(command -v python)" -elif [ -x "$(command -v python2)" ] -then - PY="$(command -v python2)" -else - echo "ERROR: No working python installation was found" - echo "Please install python and add it to the PATH variable" - exit 1 -fi - -# Function that compares two versions strings v1 and v2 given in arguments (e.g 1.31 and 1.33.0). -# Returns 1 if v1 > v2, 0 if v1 == v2, 2 if v1 < v2. -function vercomp() { - if [[ $1 == $2 ]] - then - return 0 - fi - v1=( ${1//./ } ) - v2=( ${2//./ } ) - len1=${#v1[@]} - len2=${#v2[@]} - max_len=$len1 - if [[ $max_len -lt $len2 ]] - then - max_len=$len2 - fi - - #pad right in short arr - if [[ len1 -gt len2 ]]; - then - for ((i = len2; i < len1; i++)); - do - v2[$i]=0 - done - else - for ((i = len1; i < len2; i++)); - do - v1[$i]=0 - done - fi - - for i in `seq 0 $((max_len-1))` - do - # Fill empty fields with zeros in v1 - if [ -z "${v1[$i]}" ] - then - v1[$i]=0 - fi - # And in v2 - if [ -z "${v2[$i]}" ] - then - v2[$i]=0 - fi - if [ ${v1[$i]} -gt ${v2[$i]} ] - then - return 1 - fi - if [ ${v1[$i]} -lt ${v2[$i]} ] - then - return 2 - fi - done - return 0 -} - -RustVersion=$(rustc --version | cut -d " " -f 2) -MinRustVersion=1.70 -vercomp "$RustVersion" $MinRustVersion || ec=$? -if [ ${ec:-0} -eq 2 ] -then - echo "ERROR: Rust version is too old: $RustVersion - needs at least $MinRustVersion" - echo "Please update Rust with 'rustup update'" - exit 1 -else - echo "SUCCESS: Rust is up to date" -fi - -Path=${1:-rustlings/} -echo "Cloning Rustlings at $Path..." -git clone -q https://github.com/rust-lang/rustlings.git "$Path" - -cd "$Path" - -Version=$(curl -s https://api.github.com/repos/rust-lang/rustlings/releases/latest | ${PY} -c "import json,sys;obj=json.load(sys.stdin);print(obj['tag_name']) if 'tag_name' in obj else sys.exit(f\"Error: {obj['message']}\");") -CargoBin="${CARGO_HOME:-$HOME/.cargo}/bin" - -if [[ -z ${Version} ]] -then - echo "The latest tag version could not be fetched remotely." - echo "Using the local git repository..." - Version=$(ls -tr .git/refs/tags/ | tail -1) - if [[ -z ${Version} ]] - then - echo "No valid tag version found" - echo "Rustlings will be installed using the main branch" - Version="main" - else - Version="tags/${Version}" - fi -else - Version="tags/${Version}" -fi - -echo "Checking out version $Version..." -git checkout -q ${Version} - -echo "Installing the 'rustlings' executable..." -cargo install --force --path . - -if ! [ -x "$(command -v rustlings)" ] -then - echo "WARNING: Please check that you have '$CargoBin' in your PATH environment variable!" -fi - -# Checking whether Clippy is installed. -# Due to a bug in Cargo, this must be done with Rustup: https://github.com/rust-lang/rustup/issues/1514 -Clippy=$(rustup component list | grep "clippy" | grep "installed") -if [ -z "$Clippy" ] -then - echo "Installing the 'cargo-clippy' executable..." - rustup component add clippy -fi - -echo "All done! Run 'rustlings' to get started." From 79ca821e26711123c959e919eed2a630fa102cd5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 20:48:30 +0100 Subject: [PATCH 016/433] Fix tests --- src/exercise.rs | 10 +++++----- src/main.rs | 12 +++++++++--- tests/fixture/failure/{ => exercises}/compFailure.rs | 0 .../failure/{ => exercises}/compNoExercise.rs | 0 tests/fixture/failure/{ => exercises}/testFailure.rs | 0 .../fixture/failure/{ => exercises}/testNotPassed.rs | 0 tests/fixture/failure/info.toml | 4 ++-- .../state/{ => exercises}/finished_exercise.rs | 0 .../state/{ => exercises}/pending_exercise.rs | 0 .../state/{ => exercises}/pending_test_exercise.rs | 0 tests/fixture/state/info.toml | 7 +++---- tests/fixture/success/{ => exercises}/compSuccess.rs | 0 tests/fixture/success/{ => exercises}/testSuccess.rs | 0 tests/fixture/success/info.toml | 4 ++-- 14 files changed, 21 insertions(+), 16 deletions(-) rename tests/fixture/failure/{ => exercises}/compFailure.rs (100%) rename tests/fixture/failure/{ => exercises}/compNoExercise.rs (100%) rename tests/fixture/failure/{ => exercises}/testFailure.rs (100%) rename tests/fixture/failure/{ => exercises}/testNotPassed.rs (100%) rename tests/fixture/state/{ => exercises}/finished_exercise.rs (100%) rename tests/fixture/state/{ => exercises}/pending_exercise.rs (100%) rename tests/fixture/state/{ => exercises}/pending_test_exercise.rs (100%) rename tests/fixture/success/{ => exercises}/compSuccess.rs (100%) rename tests/fixture/success/{ => exercises}/testSuccess.rs (100%) diff --git a/src/exercise.rs b/src/exercise.rs index 7c2e5fde..11259168 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -354,7 +354,7 @@ mod test { File::create(temp_file()).unwrap(); let exercise = Exercise { name: String::from("example"), - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), mode: Mode::Compile, hint: String::from(""), }; @@ -372,7 +372,7 @@ mod test { let exercise = Exercise { name: String::from("example"), // We want a file that does actually compile - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), mode: *mode, hint: String::from(""), }; @@ -385,7 +385,7 @@ mod test { fn test_pending_state() { let exercise = Exercise { name: "pending_exercise".into(), - path: PathBuf::from("tests/fixture/state/pending_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), mode: Mode::Compile, hint: String::new(), }; @@ -426,7 +426,7 @@ mod test { fn test_finished_exercise() { let exercise = Exercise { name: "finished_exercise".into(), - path: PathBuf::from("tests/fixture/state/finished_exercise.rs"), + path: PathBuf::from("tests/fixture/state/exercises/finished_exercise.rs"), mode: Mode::Compile, hint: String::new(), }; @@ -438,7 +438,7 @@ mod test { fn test_exercise_with_output() { let exercise = Exercise { name: "exercise_with_output".into(), - path: PathBuf::from("tests/fixture/success/testSuccess.rs"), + path: PathBuf::from("tests/fixture/success/exercises/testSuccess.rs"), mode: Mode::Test, hint: String::new(), }; diff --git a/src/main.rs b/src/main.rs index 76b63736..2ac44d55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; +use std::fs; use std::io::{self, prelude::*}; use std::path::Path; use std::process::{exit, Command}; @@ -100,9 +101,14 @@ fn main() -> Result<()> { std::process::exit(1); } - let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - .unwrap() - .exercises; + // Read a local `info.toml` if it exists. Mainly to let the tests work for now. + let exercises = if let Ok(file_content) = fs::read_to_string("info.toml") { + toml_edit::de::from_str::(&file_content) + } else { + toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) + } + .context("Failed to parse `info.toml`")? + .exercises; if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; diff --git a/tests/fixture/failure/compFailure.rs b/tests/fixture/failure/exercises/compFailure.rs similarity index 100% rename from tests/fixture/failure/compFailure.rs rename to tests/fixture/failure/exercises/compFailure.rs diff --git a/tests/fixture/failure/compNoExercise.rs b/tests/fixture/failure/exercises/compNoExercise.rs similarity index 100% rename from tests/fixture/failure/compNoExercise.rs rename to tests/fixture/failure/exercises/compNoExercise.rs diff --git a/tests/fixture/failure/testFailure.rs b/tests/fixture/failure/exercises/testFailure.rs similarity index 100% rename from tests/fixture/failure/testFailure.rs rename to tests/fixture/failure/exercises/testFailure.rs diff --git a/tests/fixture/failure/testNotPassed.rs b/tests/fixture/failure/exercises/testNotPassed.rs similarity index 100% rename from tests/fixture/failure/testNotPassed.rs rename to tests/fixture/failure/exercises/testNotPassed.rs diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml index e5949f9b..9474ee3f 100644 --- a/tests/fixture/failure/info.toml +++ b/tests/fixture/failure/info.toml @@ -1,11 +1,11 @@ [[exercises]] name = "compFailure" -path = "compFailure.rs" +path = "exercises/compFailure.rs" mode = "compile" hint = "" [[exercises]] name = "testFailure" -path = "testFailure.rs" +path = "exercises/testFailure.rs" mode = "test" hint = "Hello!" diff --git a/tests/fixture/state/finished_exercise.rs b/tests/fixture/state/exercises/finished_exercise.rs similarity index 100% rename from tests/fixture/state/finished_exercise.rs rename to tests/fixture/state/exercises/finished_exercise.rs diff --git a/tests/fixture/state/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs similarity index 100% rename from tests/fixture/state/pending_exercise.rs rename to tests/fixture/state/exercises/pending_exercise.rs diff --git a/tests/fixture/state/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs similarity index 100% rename from tests/fixture/state/pending_test_exercise.rs rename to tests/fixture/state/exercises/pending_test_exercise.rs diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml index 547b3a48..8de5d604 100644 --- a/tests/fixture/state/info.toml +++ b/tests/fixture/state/info.toml @@ -1,18 +1,17 @@ [[exercises]] name = "pending_exercise" -path = "pending_exercise.rs" +path = "exercises/pending_exercise.rs" mode = "compile" hint = """""" [[exercises]] name = "pending_test_exercise" -path = "pending_test_exercise.rs" +path = "exercises/pending_test_exercise.rs" mode = "test" hint = """""" [[exercises]] name = "finished_exercise" -path = "finished_exercise.rs" +path = "exercises/finished_exercise.rs" mode = "compile" hint = """""" - diff --git a/tests/fixture/success/compSuccess.rs b/tests/fixture/success/exercises/compSuccess.rs similarity index 100% rename from tests/fixture/success/compSuccess.rs rename to tests/fixture/success/exercises/compSuccess.rs diff --git a/tests/fixture/success/testSuccess.rs b/tests/fixture/success/exercises/testSuccess.rs similarity index 100% rename from tests/fixture/success/testSuccess.rs rename to tests/fixture/success/exercises/testSuccess.rs diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml index 68d35388..17ed8c62 100644 --- a/tests/fixture/success/info.toml +++ b/tests/fixture/success/info.toml @@ -1,11 +1,11 @@ [[exercises]] name = "compSuccess" -path = "compSuccess.rs" +path = "exercises/compSuccess.rs" mode = "compile" hint = """""" [[exercises]] name = "testSuccess" -path = "testSuccess.rs" +path = "exercises/testSuccess.rs" mode = "test" hint = """""" From 23f0fae1c8eddfa1ac679d8167ec63b554c554b9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 21:13:28 +0100 Subject: [PATCH 017/433] Show a success message after resetting --- src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2ac44d55..1926f6ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -212,19 +212,17 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Subcommands::Run { name } => { let exercise = find_exercise(&name, &exercises); - run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); } Subcommands::Reset { name } => { let exercise = find_exercise(&name, &exercises); - reset(exercise)?; + println!("The file {} has been reset!", exercise.path.display()); } Subcommands::Hint { name } => { let exercise = find_exercise(&name, &exercises); - println!("{}", exercise.hint); } From b5e17c965d1fee01336fdfabd93c575555a44d62 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 30 Mar 2024 21:15:11 +0100 Subject: [PATCH 018/433] Add an error message when a file is not embedded --- src/embedded.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/embedded.rs b/src/embedded.rs index 25dbe641..f65b8aef 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -113,6 +113,9 @@ impl EmbeddedFiles { } } - Err(io::Error::from(io::ErrorKind::NotFound)) + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("{} not found in the embedded files", path.display()), + )) } } From 1e1f0317134fc3588f2eea4a118bd72aba3f9b34 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 00:49:19 +0100 Subject: [PATCH 019/433] Fix path comparison --- src/embedded.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index f65b8aef..56b4b618 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -97,17 +97,13 @@ impl EmbeddedFiles { .exercises_dir .files .iter() - .find(|file| file.path == path.as_os_str()) + .find(|file| Path::new(file.path) == path) { return file.write_to_disk(strategy); } for dir in self.exercises_dir.dirs { - if let Some(file) = dir - .content - .iter() - .find(|file| file.path == path.as_os_str()) - { + if let Some(file) = dir.content.iter().find(|file| Path::new(file.path) == path) { dir.init_on_disk()?; return file.write_to_disk(strategy); } From b711dd692afaf42830efb04c491616d3f069fbdf Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 02:04:41 +0100 Subject: [PATCH 020/433] Add .gitignore --- src/init.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/init.rs b/src/init.rs index 1edcb23c..d958c96e 100644 --- a/src/init.rs +++ b/src/init.rs @@ -32,6 +32,15 @@ publish = false .write_all(&cargo_toml) } +fn create_gitignore() -> io::Result<()> { + let gitignore = b"/target"; + OpenOptions::new() + .create_new(true) + .write(true) + .open(".gitignore")? + .write_all(gitignore) +} + fn create_vscode_dir() -> Result<()> { create_dir(".vscode").context("Failed to create the directory `.vscode`")?; let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; @@ -77,6 +86,8 @@ Then run `rustlings` again" create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; + create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; Ok(()) From 82b563f1654860ba3590d91ec3c0f321e3130ae2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 16:55:33 +0200 Subject: [PATCH 021/433] Use Cargo instead of rustc --- src/exercise.rs | 262 +++++++++++------------------------------------- src/main.rs | 25 ++--- src/run.rs | 53 +++------- src/verify.rs | 119 +++++++++------------- 4 files changed, 132 insertions(+), 327 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 11259168..83d444fc 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,21 +1,21 @@ +use anyhow::{Context, Result}; use serde::Deserialize; -use std::fmt::{self, Display, Formatter}; -use std::fs::{self, remove_file, File}; +use std::fmt::{self, Debug, Display, Formatter}; +use std::fs::{self, File}; use std::io::{self, BufRead, BufReader}; use std::path::PathBuf; -use std::process::{self, exit, Command, Stdio}; -use std::{array, env, mem}; +use std::process::{exit, Command, Output}; +use std::{array, mem}; use winnow::ascii::{space0, Caseless}; use winnow::combinator::opt; use winnow::Parser; -const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; -const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; -const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; -const CONTEXT: usize = 2; -const CLIPPY_CARGO_TOML_PATH: &str = "exercises/22_clippy/Cargo.toml"; +use crate::embedded::EMBEDDED_FILES; -// Checks if the line contains the "I AM NOT DONE" comment. +// The number of context lines above and below a highlighted line. +const CONTEXT: usize = 2; + +// Check if the line contains the "I AM NOT DONE" comment. fn contains_not_done_comment(input: &str) -> bool { ( space0::<_, ()>, @@ -28,26 +28,15 @@ fn contains_not_done_comment(input: &str) -> bool { .is_ok() } -// Get a temporary file name that is hopefully unique -#[inline] -fn temp_file() -> String { - let thread_id: String = format!("{:?}", std::thread::current().id()) - .chars() - .filter(|c| c.is_alphanumeric()) - .collect(); - - format!("./temp_{}_{thread_id}", process::id()) -} - // The mode of the exercise. -#[derive(Deserialize, Copy, Clone, Debug)] +#[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum Mode { - // Indicates that the exercise should be compiled as a binary + // The exercise should be compiled as a binary Compile, - // Indicates that the exercise should be compiled as a test harness + // The exercise should be compiled as a test harness Test, - // Indicates that the exercise should be linted with clippy + // The exercise should be linted with clippy Clippy, } @@ -56,171 +45,72 @@ pub struct ExerciseList { pub exercises: Vec, } -// A representation of a rustlings exercise. -// This is deserialized from the accompanying info.toml file -#[derive(Deserialize, Debug)] +impl ExerciseList { + pub fn parse() -> Result { + // Read a local `info.toml` if it exists. + // Mainly to let the tests work for now. + if let Ok(file_content) = fs::read_to_string("info.toml") { + toml_edit::de::from_str(&file_content) + } else { + toml_edit::de::from_str(EMBEDDED_FILES.info_toml_content) + } + .context("Failed to parse `info.toml`") + } +} + +// Deserialized from the `info.toml` file. +#[derive(Deserialize)] pub struct Exercise { // Name of the exercise pub name: String, // The path to the file containing the exercise's source code pub path: PathBuf, - // The mode of the exercise (Test, Compile, or Clippy) + // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise pub hint: String, } -// An enum to track of the state of an Exercise. -// An Exercise can be either Done or Pending +// The state of an Exercise. #[derive(PartialEq, Eq, Debug)] pub enum State { - // The state of the exercise once it's been completed Done, - // The state of the exercise while it's not completed yet Pending(Vec), } -// The context information of a pending exercise +// The context information of a pending exercise. #[derive(PartialEq, Eq, Debug)] pub struct ContextLine { - // The source code that is still pending completion + // The source code line pub line: String, - // The line number of the source code still pending completion + // The line number pub number: usize, - // Whether or not this is important + // Whether this is important and should be highlighted pub important: bool, } -// The result of compiling an exercise -pub struct CompiledExercise<'a> { - exercise: &'a Exercise, - _handle: FileHandle, -} - -impl<'a> CompiledExercise<'a> { - // Run the compiled exercise - pub fn run(&self) -> Result { - self.exercise.run() - } -} - -// A representation of an already executed binary -#[derive(Debug)] -pub struct ExerciseOutput { - // The textual contents of the standard output of the binary - pub stdout: String, - // The textual contents of the standard error of the binary - pub stderr: String, -} - -struct FileHandle; - -impl Drop for FileHandle { - fn drop(&mut self) { - clean(); - } -} - impl Exercise { - pub fn compile(&self) -> Result { - let cmd = match self.mode { - Mode::Compile => Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Test => Command::new("rustc") - .args(["--test", self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Clippy => { - let cargo_toml = format!( - r#"[package] -name = "{}" -version = "0.0.1" -edition = "2021" -[[bin]] -name = "{}" -path = "{}.rs""#, - self.name, self.name, self.name - ); - let cargo_toml_error_msg = if env::var("NO_EMOJI").is_ok() { - "Failed to write Clippy Cargo.toml file." - } else { - "Failed to write 📎 Clippy 📎 Cargo.toml file." - }; - fs::write(CLIPPY_CARGO_TOML_PATH, cargo_toml).expect(cargo_toml_error_msg); - // To support the ability to run the clippy exercises, build - // an executable, in addition to running clippy. With a - // compilation failure, this would silently fail. But we expect - // clippy to reflect the same failure while compiling later. - Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to compile!"); - // Due to an issue with Clippy, a cargo clean is required to catch all lints. - // See https://github.com/rust-lang/rust-clippy/issues/2604 - // This is already fixed on Clippy's master branch. See this issue to track merging into Cargo: - // https://github.com/rust-lang/rust-clippy/issues/3837 - Command::new("cargo") - .args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to run 'cargo clean'"); - Command::new("cargo") - .args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .args(["--", "-D", "warnings", "-D", "clippy::float_cmp"]) - .output() - } - } - .expect("Failed to run 'compile' command."); - - if cmd.status.success() { - Ok(CompiledExercise { - exercise: self, - _handle: FileHandle, - }) - } else { - clean(); - Err(ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }) - } + fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { + Command::new("cargo") + .arg(command) + .arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(&self.name) + .args(args) + .output() + .context("Failed to run Cargo") } - fn run(&self) -> Result { - let arg = match self.mode { - Mode::Test => "--show-output", - _ => "", - }; - let cmd = Command::new(temp_file()) - .arg(arg) - .output() - .expect("Failed to run 'run' command"); - - let output = ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }; - - if cmd.status.success() { - Ok(output) - } else { - Err(output) + pub fn run(&self) -> Result { + match self.mode { + Mode::Compile => self.cargo_cmd("run", &[]), + Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]), + Mode::Clippy => self.cargo_cmd( + "clippy", + &["--", "-D", "warnings", "-D", "clippy::float_cmp"], + ), } } @@ -335,51 +225,13 @@ path = "{}.rs""#, impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.path.to_str().unwrap()) + self.path.fmt(f) } } -#[inline] -fn clean() { - let _ignored = remove_file(temp_file()); -} - #[cfg(test)] mod test { use super::*; - use std::path::Path; - - #[test] - fn test_clean() { - File::create(temp_file()).unwrap(); - let exercise = Exercise { - name: String::from("example"), - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::from(""), - }; - let compiled = exercise.compile().unwrap(); - drop(compiled); - assert!(!Path::new(&temp_file()).exists()); - } - - #[test] - #[cfg(target_os = "windows")] - fn test_no_pdb_file() { - [Mode::Compile, Mode::Test] // Clippy doesn't like to test - .iter() - .for_each(|mode| { - let exercise = Exercise { - name: String::from("example"), - // We want a file that does actually compile - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: *mode, - hint: String::from(""), - }; - let _ = exercise.compile().unwrap(); - assert!(!Path::new(&format!("{}.pdb", temp_file())).exists()); - }); - } #[test] fn test_pending_state() { @@ -442,8 +294,8 @@ mod test { mode: Mode::Test, hint: String::new(), }; - let out = exercise.compile().unwrap().run().unwrap(); - assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); + let out = exercise.run().unwrap(); + assert_eq!(out.stdout, b"THIS TEST TOO SHALL PASS"); } #[test] diff --git a/src/main.rs b/src/main.rs index 1926f6ac..1c736f31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,20 +4,18 @@ use crate::verify::verify; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; -use embedded::EMBEDDED_FILES; use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::ffi::OsStr; -use std::fs; -use std::io::{self, prelude::*}; +use std::io::{BufRead, Write}; use std::path::Path; use std::process::{exit, Command}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; -use std::thread; use std::time::Duration; +use std::{io, thread}; #[macro_use] mod ui; @@ -94,21 +92,16 @@ fn main() -> Result<()> { println!("\n{WELCOME}\n"); } - if which::which("rustc").is_err() { - println!("We cannot find `rustc`."); - println!("Try running `rustc --version` to diagnose your problem."); - println!("For instructions on how to install Rust, check the README."); + if which::which("cargo").is_err() { + println!( + "Failed to find `cargo`. +Did you already install Rust? +Try running `cargo --version` to diagnose the problem." + ); std::process::exit(1); } - // Read a local `info.toml` if it exists. Mainly to let the tests work for now. - let exercises = if let Ok(file_content) = fs::read_to_string("info.toml") { - toml_edit::de::from_str::(&file_content) - } else { - toml_edit::de::from_str::(EMBEDDED_FILES.info_toml_content) - } - .context("Failed to parse `info.toml`")? - .exercises; + let exercises = ExerciseList::parse()?.exercises; if matches!(args.command, Some(Subcommands::Init)) { init::init_rustlings(&exercises).context("Initialization failed")?; diff --git a/src/run.rs b/src/run.rs index 792bd8fd..2c9f99f6 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,4 +1,5 @@ -use std::io; +use anyhow::{bail, Result}; +use std::io::{self, stdout, Write}; use std::time::Duration; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; @@ -10,13 +11,11 @@ use indicatif::ProgressBar; // and run the ensuing binary. // The verbose argument helps determine whether or not to show // the output from the test harnesses (if the mode of the exercise is test) -pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { +pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> { match exercise.mode { - Mode::Test => test(exercise, verbose)?, - Mode::Compile => compile_and_run(exercise)?, - Mode::Clippy => compile_and_run(exercise)?, + Mode::Test => test(exercise, verbose), + Mode::Compile | Mode::Clippy => compile_and_run(exercise), } - Ok(()) } // Resets the exercise by stashing the changes. @@ -27,41 +26,21 @@ pub fn reset(exercise: &Exercise) -> io::Result<()> { // Invoke the rust compiler on the path of the given exercise // and run the ensuing binary. // This is strictly for non-test binaries, so output is displayed -fn compile_and_run(exercise: &Exercise) -> Result<(), ()> { +fn compile_and_run(exercise: &Exercise) -> Result<()> { let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); + progress_bar.set_message(format!("Running {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let compilation_result = exercise.compile(); - let compilation = match compilation_result { - Ok(compilation) => compilation, - Err(output) => { - progress_bar.finish_and_clear(); - warn!( - "Compilation of {} failed!, Compiler error message:\n", - exercise - ); - println!("{}", output.stderr); - return Err(()); - } - }; - - progress_bar.set_message(format!("Running {exercise}...")); - let result = compilation.run(); + let output = exercise.run()?; progress_bar.finish_and_clear(); - match result { - Ok(output) => { - println!("{}", output.stdout); - success!("Successfully ran {}", exercise); - Ok(()) - } - Err(output) => { - println!("{}", output.stdout); - println!("{}", output.stderr); - - warn!("Ran {} with errors", exercise); - Err(()) - } + stdout().write_all(&output.stdout)?; + if !output.status.success() { + stdout().write_all(&output.stderr)?; + warn!("Ran {} with errors", exercise); + bail!("TODO"); } + + success!("Successfully ran {}", exercise); + Ok(()) } diff --git a/src/verify.rs b/src/verify.rs index 5275bf78..56c67796 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,7 +1,14 @@ -use crate::exercise::{CompiledExercise, Exercise, Mode, State}; +use anyhow::{bail, Result}; use console::style; use indicatif::{ProgressBar, ProgressStyle}; -use std::{env, time::Duration}; +use std::{ + env, + io::{stdout, Write}, + process::Output, + time::Duration, +}; + +use crate::exercise::{Exercise, Mode, State}; // Verify that the provided container of Exercise objects // can be compiled and run without any failures. @@ -58,50 +65,44 @@ enum RunMode { } // Compile and run the resulting test harness of the given Exercise -pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> { +pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> { compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?; Ok(()) } // Invoke the rust compiler without running the resulting binary -fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { +fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Compiling {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let _ = compile(exercise, &progress_bar)?; + let _ = exercise.run()?; progress_bar.finish_and_clear(); Ok(prompt_for_completion(exercise, None, success_hints)) } // Compile the given Exercise and run the resulting binary in an interactive mode -fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { +fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); + progress_bar.set_message(format!("Running {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let compilation = compile(exercise, &progress_bar)?; - - progress_bar.set_message(format!("Running {exercise}...")); - let result = compilation.run(); + let output = exercise.run()?; progress_bar.finish_and_clear(); - let output = match result { - Ok(output) => output, - Err(output) => { - warn!("Ran {} with errors", exercise); - println!("{}", output.stdout); - println!("{}", output.stderr); - return Err(()); + if !output.status.success() { + warn!("Ran {} with errors", exercise); + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; } - }; + bail!("TODO"); + } - Ok(prompt_for_completion( - exercise, - Some(output.stdout), - success_hints, - )) + Ok(prompt_for_completion(exercise, Some(output), success_hints)) } // Compile the given Exercise as a test harness and display @@ -111,62 +112,42 @@ fn compile_and_test( run_mode: RunMode, verbose: bool, success_hints: bool, -) -> Result { +) -> Result { let progress_bar = ProgressBar::new_spinner(); progress_bar.set_message(format!("Testing {exercise}...")); progress_bar.enable_steady_tick(Duration::from_millis(100)); - let compilation = compile(exercise, &progress_bar)?; - let result = compilation.run(); + let output = exercise.run()?; progress_bar.finish_and_clear(); - match result { - Ok(output) => { - if verbose { - println!("{}", output.stdout); - } - if run_mode == RunMode::Interactive { - Ok(prompt_for_completion(exercise, None, success_hints)) - } else { - Ok(true) - } - } - Err(output) => { - warn!( - "Testing of {} failed! Please try again. Here's the output:", - exercise - ); - println!("{}", output.stdout); - Err(()) + if !output.status.success() { + warn!( + "Testing of {} failed! Please try again. Here's the output:", + exercise + ); + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; } + bail!("TODO"); } -} -// Compile the given Exercise and return an object with information -// about the state of the compilation -fn compile<'a>( - exercise: &'a Exercise, - progress_bar: &ProgressBar, -) -> Result, ()> { - let compilation_result = exercise.compile(); + if verbose { + stdout().write_all(&output.stdout)?; + } - match compilation_result { - Ok(compilation) => Ok(compilation), - Err(output) => { - progress_bar.finish_and_clear(); - warn!( - "Compiling of {} failed! Please try again. Here's the output:", - exercise - ); - println!("{}", output.stderr); - Err(()) - } + if run_mode == RunMode::Interactive { + Ok(prompt_for_completion(exercise, None, success_hints)) + } else { + Ok(true) } } fn prompt_for_completion( exercise: &Exercise, - prompt_output: Option, + prompt_output: Option, success_hints: bool, ) -> bool { let context = match exercise.state() { @@ -200,10 +181,10 @@ fn prompt_for_completion( } if let Some(output) = prompt_output { - println!( - "Output:\n{separator}\n{output}\n{separator}\n", - separator = separator(), - ); + let separator = separator(); + println!("Output:\n{separator}"); + stdout().write_all(&output.stdout).unwrap(); + println!("\n{separator}\n"); } if success_hints { println!( From c1de4d46aad38d315e061b7262f773f48c6aab63 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 18:25:54 +0200 Subject: [PATCH 022/433] Some improvements to error handling --- src/exercise.rs | 23 ++++++-------- src/main.rs | 84 +++++++++++++++++++++++-------------------------- src/verify.rs | 14 ++++----- 3 files changed, 55 insertions(+), 66 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 83d444fc..48aaedd0 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -114,14 +114,9 @@ impl Exercise { } } - pub fn state(&self) -> State { - let source_file = File::open(&self.path).unwrap_or_else(|e| { - println!( - "Failed to open the exercise file {}: {e}", - self.path.display(), - ); - exit(1); - }); + pub fn state(&self) -> Result { + let source_file = File::open(&self.path) + .with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?; let mut source_reader = BufReader::new(source_file); // Read the next line into `buf` without the newline at the end. @@ -152,7 +147,7 @@ impl Exercise { // Reached the end of the file and didn't find the comment. if n == 0 { - return State::Done; + return Ok(State::Done); } if contains_not_done_comment(&line) { @@ -198,7 +193,7 @@ impl Exercise { }); } - return State::Pending(context); + return Ok(State::Pending(context)); } current_line_number += 1; @@ -218,8 +213,8 @@ impl Exercise { // without actually having solved anything. // The only other way to truly check this would to compile and run // the exercise; which would be both costly and counterintuitive - pub fn looks_done(&self) -> bool { - self.state() == State::Done + pub fn looks_done(&self) -> Result { + self.state().map(|state| state == State::Done) } } @@ -271,7 +266,7 @@ mod test { }, ]; - assert_eq!(state, State::Pending(expected)); + assert_eq!(state.unwrap(), State::Pending(expected)); } #[test] @@ -283,7 +278,7 @@ mod test { hint: String::new(), }; - assert_eq!(exercise.state(), State::Done); + assert_eq!(exercise.state().unwrap(), State::Done); } #[test] diff --git a/src/main.rs b/src/main.rs index 1c736f31..72bff4d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,14 +92,11 @@ fn main() -> Result<()> { println!("\n{WELCOME}\n"); } - if which::which("cargo").is_err() { - println!( - "Failed to find `cargo`. + which::which("cargo").context( + "Failed to find `cargo`. Did you already install Rust? -Try running `cargo --version` to diagnose the problem." - ); - std::process::exit(1); - } +Try running `cargo --version` to diagnose the problem.", + )?; let exercises = ExerciseList::parse()?.exercises; @@ -122,7 +119,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); - std::process::exit(0); + exit(0); }); match command { @@ -160,7 +157,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let filter_cond = filters .iter() .any(|f| exercise.name.contains(f) || fname.contains(f)); - let looks_done = exercise.looks_done(); + let looks_done = exercise.looks_done()?; let status = if looks_done { exercises_done += 1; "Done" @@ -185,8 +182,8 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let mut handle = stdout.lock(); handle.write_all(line.as_bytes()).unwrap_or_else(|e| { match e.kind() { - std::io::ErrorKind::BrokenPipe => std::process::exit(0), - _ => std::process::exit(1), + std::io::ErrorKind::BrokenPipe => exit(0), + _ => exit(1), }; }); } @@ -200,35 +197,34 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exercises.len(), percentage_progress ); - std::process::exit(0); + exit(0); } Subcommands::Run { name } => { - let exercise = find_exercise(&name, &exercises); - run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1)); + let exercise = find_exercise(&name, &exercises)?; + run(exercise, verbose).unwrap_or_else(|_| exit(1)); } Subcommands::Reset { name } => { - let exercise = find_exercise(&name, &exercises); + let exercise = find_exercise(&name, &exercises)?; reset(exercise)?; println!("The file {} has been reset!", exercise.path.display()); } Subcommands::Hint { name } => { - let exercise = find_exercise(&name, &exercises); + let exercise = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } Subcommands::Verify => { - verify(&exercises, (0, exercises.len()), verbose, false) - .unwrap_or_else(|_| std::process::exit(1)); + verify(&exercises, (0, exercises.len()), verbose, false).unwrap_or_else(|_| exit(1)); } Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Err(e) => { println!("Error: Could not watch your progress. Error message was {e:?}."); println!("Most likely you've run out of disk space or your 'inotify limit' has been reached."); - std::process::exit(1); + exit(1); } Ok(WatchStatus::Finished) => { println!( @@ -295,25 +291,23 @@ fn spawn_watch_shell( }); } -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise { +fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> { if name == "next" { - exercises - .iter() - .find(|e| !e.looks_done()) - .unwrap_or_else(|| { - println!("🎉 Congratulations! You have done all the exercises!"); - println!("🔚 There are no more exercises to do next!"); - std::process::exit(1) - }) - } else { - exercises - .iter() - .find(|e| e.name == name) - .unwrap_or_else(|| { - println!("No exercise found for '{name}'!"); - std::process::exit(1) - }) + for exercise in exercises { + if !exercise.looks_done()? { + return Ok(exercise); + } + } + + println!("🎉 Congratulations! You have done all the exercises!"); + println!("🔚 There are no more exercises to do next!"); + exit(0); } + + exercises + .iter() + .find(|e| e.name == name) + .with_context(|| format!("No exercise found for '{name}'!")) } enum WatchStatus { @@ -363,17 +357,17 @@ fn watch( && event_path.exists() { let filepath = event_path.as_path().canonicalize().unwrap(); - let pending_exercises = - exercises - .iter() - .find(|e| filepath.ends_with(&e.path)) - .into_iter() - .chain(exercises.iter().filter(|e| { - !e.looks_done() && !filepath.ends_with(&e.path) - })); + // TODO: Remove unwrap + let pending_exercises = exercises + .iter() + .find(|e| filepath.ends_with(&e.path)) + .into_iter() + .chain(exercises.iter().filter(|e| { + !e.looks_done().unwrap() && !filepath.ends_with(&e.path) + })); let num_done = exercises .iter() - .filter(|e| e.looks_done() && !filepath.ends_with(&e.path)) + .filter(|e| e.looks_done().unwrap() && !filepath.ends_with(&e.path)) .count(); clear_screen(); match verify( diff --git a/src/verify.rs b/src/verify.rs index 56c67796..adfd3b26 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -79,7 +79,7 @@ fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { let _ = exercise.run()?; progress_bar.finish_and_clear(); - Ok(prompt_for_completion(exercise, None, success_hints)) + prompt_for_completion(exercise, None, success_hints) } // Compile the given Exercise and run the resulting binary in an interactive mode @@ -102,7 +102,7 @@ fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Re bail!("TODO"); } - Ok(prompt_for_completion(exercise, Some(output), success_hints)) + prompt_for_completion(exercise, Some(output), success_hints) } // Compile the given Exercise as a test harness and display @@ -139,7 +139,7 @@ fn compile_and_test( } if run_mode == RunMode::Interactive { - Ok(prompt_for_completion(exercise, None, success_hints)) + prompt_for_completion(exercise, None, success_hints) } else { Ok(true) } @@ -149,9 +149,9 @@ fn prompt_for_completion( exercise: &Exercise, prompt_output: Option, success_hints: bool, -) -> bool { - let context = match exercise.state() { - State::Done => return true, +) -> Result { + let context = match exercise.state()? { + State::Done => return Ok(true), State::Pending(context) => context, }; match exercise.mode { @@ -215,7 +215,7 @@ fn prompt_for_completion( ); } - false + Ok(false) } fn separator() -> console::StyledObject<&'static str> { From 7090fffeae88a2afdeb42ae3301c4842416ab729 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 18:59:01 +0200 Subject: [PATCH 023/433] Fix tests --- .gitignore | 7 +++---- Cargo.toml | 5 +++++ tests/fixture/failure/Cargo.toml | 21 +++++++++++++++++++++ tests/fixture/state/Cargo.toml | 17 +++++++++++++++++ tests/fixture/success/Cargo.toml | 13 +++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/fixture/failure/Cargo.toml create mode 100644 tests/fixture/state/Cargo.toml create mode 100644 tests/fixture/success/Cargo.toml diff --git a/.gitignore b/.gitignore index f319d39d..d6c77083 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ -*.swp target/ +tests/fixture/*/Cargo.lock + +*.swp **/*.rs.bk .DS_Store *.pdb -exercises/clippy/Cargo.toml -exercises/clippy/Cargo.lock -rust-project.json .idea .vscode/* !.vscode/extensions.json diff --git a/Cargo.toml b/Cargo.toml index 9224364b..5fc75f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] resolver = "2" +exclude = [ + "tests/fixture/failure", + "tests/fixture/state", + "tests/fixture/success", +] [workspace.package] version = "6.0.0" diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml new file mode 100644 index 00000000..dd728c34 --- /dev/null +++ b/tests/fixture/failure/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "compFailure" +path = "exercises/compFailure.rs" + +[[bin]] +name = "compNoExercise" +path = "exercises/compNoExercise.rs" + +[[bin]] +name = "testFailure" +path = "exercises/testFailure.rs" + +[[bin]] +name = "testNotPassed" +path = "exercises/testNotPassed.rs" diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml new file mode 100644 index 00000000..5cfa42ba --- /dev/null +++ b/tests/fixture/state/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "pending_exercise" +path = "exercises/pending_exercise.rs" + +[[bin]] +name = "pending_test_exercise" +path = "exercises/pending_test_exercise.rs" + +[[bin]] +name = "finished_exercise" +path = "exercises/finished_exercise.rs" diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml new file mode 100644 index 00000000..c0059284 --- /dev/null +++ b/tests/fixture/success/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tests" +version = "0.0.0" +edition = "2021" +publish = false + +[[bin]] +name = "compSuccess" +path = "exercises/compSuccess.rs" + +[[bin]] +name = "testSuccess" +path = "exercises/testSuccess.rs" From fb32d0b86fd2f3f0c1e82fecbf2cf4931a7b1ff5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 18:59:07 +0200 Subject: [PATCH 024/433] Remove redundant test --- src/exercise.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 48aaedd0..e7045d60 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -281,18 +281,6 @@ mod test { assert_eq!(exercise.state().unwrap(), State::Done); } - #[test] - fn test_exercise_with_output() { - let exercise = Exercise { - name: "exercise_with_output".into(), - path: PathBuf::from("tests/fixture/success/exercises/testSuccess.rs"), - mode: Mode::Test, - hint: String::new(), - }; - let out = exercise.run().unwrap(); - assert_eq!(out.stdout, b"THIS TEST TOO SHALL PASS"); - } - #[test] fn test_not_done() { assert!(contains_not_done_comment("// I AM NOT DONE")); From 7560aec66b4a109c32ea59daa65580ab2ac26333 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 20:08:23 +0200 Subject: [PATCH 025/433] Inline reset --- src/main.rs | 7 +++++-- src/run.rs | 11 ++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 72bff4d9..0f298dde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; -use crate::run::{reset, run}; +use crate::run::run; use crate::verify::verify; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; @@ -207,7 +208,9 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Subcommands::Reset { name } => { let exercise = find_exercise(&name, &exercises)?; - reset(exercise)?; + EMBEDDED_FILES + .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) + .with_context(|| format!("Failed to reset the exercise {exercise}"))?; println!("The file {} has been reset!", exercise.path.display()); } diff --git a/src/run.rs b/src/run.rs index 2c9f99f6..3f93f146 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,8 +1,7 @@ use anyhow::{bail, Result}; -use std::io::{self, stdout, Write}; +use std::io::{stdout, Write}; use std::time::Duration; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, Mode}; use crate::verify::test; use indicatif::ProgressBar; @@ -18,13 +17,7 @@ pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> { } } -// Resets the exercise by stashing the changes. -pub fn reset(exercise: &Exercise) -> io::Result<()> { - EMBEDDED_FILES.write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) -} - -// Invoke the rust compiler on the path of the given exercise -// and run the ensuing binary. +// Compile and run an exercise. // This is strictly for non-test binaries, so output is displayed fn compile_and_run(exercise: &Exercise) -> Result<()> { let progress_bar = ProgressBar::new_spinner(); From 8ad18de54cdad2e94d40d7d4cb67e4a6a274c293 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 20:11:08 +0200 Subject: [PATCH 026/433] Use var_os to avoid conversion to String --- src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index d8177b9f..22d60d96 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,7 +3,7 @@ macro_rules! print_emoji { use console::{style, Emoji}; use std::env; let formatstr = format!($fmt, $ex); - if env::var("NO_EMOJI").is_ok() { + if env::var_os("NO_EMOJI").is_some() { println!("{} {}", style($sign).$color(), style(formatstr).$color()); } else { println!( From 14f3585816ae12091956efcc45c1e4aefc2f91ce Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 02:11:52 +0200 Subject: [PATCH 027/433] Make `cargo run` work --- .gitignore | 3 +- Cargo.toml | 2 + dev/Cargo.toml | 104 ++++++++++++++++++++++++++++++++++ src/bin/gen-dev-cargo-toml.rs | 56 ++++++++++++++++++ src/exercise.rs | 14 ++++- tests/dev_cargo_bins.rs | 39 +++++++++++++ 6 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 dev/Cargo.toml create mode 100644 src/bin/gen-dev-cargo-toml.rs create mode 100644 tests/dev_cargo_bins.rs diff --git a/.gitignore b/.gitignore index d6c77083..0bbbc542 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ -tests/fixture/*/Cargo.lock +/tests/fixture/*/Cargo.lock +/dev/Cargo.lock *.swp **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index 5fc75f98..86187b4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ exclude = [ "tests/fixture/failure", "tests/fixture/state", "tests/fixture/success", + "dev", ] [workspace.package] @@ -18,6 +19,7 @@ edition = "2021" [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" +default-run = "rustlings" version.workspace = true authors.workspace = true license.workspace = true diff --git a/dev/Cargo.toml b/dev/Cargo.toml new file mode 100644 index 00000000..4ad48866 --- /dev/null +++ b/dev/Cargo.toml @@ -0,0 +1,104 @@ +bin = [ + { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, + { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, + { name = "variables1", path = "../exercises/01_variables/variables1.rs" }, + { name = "variables2", path = "../exercises/01_variables/variables2.rs" }, + { name = "variables3", path = "../exercises/01_variables/variables3.rs" }, + { name = "variables4", path = "../exercises/01_variables/variables4.rs" }, + { name = "variables5", path = "../exercises/01_variables/variables5.rs" }, + { name = "variables6", path = "../exercises/01_variables/variables6.rs" }, + { name = "functions1", path = "../exercises/02_functions/functions1.rs" }, + { name = "functions2", path = "../exercises/02_functions/functions2.rs" }, + { name = "functions3", path = "../exercises/02_functions/functions3.rs" }, + { name = "functions4", path = "../exercises/02_functions/functions4.rs" }, + { name = "functions5", path = "../exercises/02_functions/functions5.rs" }, + { name = "if1", path = "../exercises/03_if/if1.rs" }, + { name = "if2", path = "../exercises/03_if/if2.rs" }, + { name = "if3", path = "../exercises/03_if/if3.rs" }, + { name = "quiz1", path = "../exercises/quiz1.rs" }, + { name = "primitive_types1", path = "../exercises/04_primitive_types/primitive_types1.rs" }, + { name = "primitive_types2", path = "../exercises/04_primitive_types/primitive_types2.rs" }, + { name = "primitive_types3", path = "../exercises/04_primitive_types/primitive_types3.rs" }, + { name = "primitive_types4", path = "../exercises/04_primitive_types/primitive_types4.rs" }, + { name = "primitive_types5", path = "../exercises/04_primitive_types/primitive_types5.rs" }, + { name = "primitive_types6", path = "../exercises/04_primitive_types/primitive_types6.rs" }, + { name = "vecs1", path = "../exercises/05_vecs/vecs1.rs" }, + { name = "vecs2", path = "../exercises/05_vecs/vecs2.rs" }, + { name = "move_semantics1", path = "../exercises/06_move_semantics/move_semantics1.rs" }, + { name = "move_semantics2", path = "../exercises/06_move_semantics/move_semantics2.rs" }, + { name = "move_semantics3", path = "../exercises/06_move_semantics/move_semantics3.rs" }, + { name = "move_semantics4", path = "../exercises/06_move_semantics/move_semantics4.rs" }, + { name = "move_semantics5", path = "../exercises/06_move_semantics/move_semantics5.rs" }, + { name = "move_semantics6", path = "../exercises/06_move_semantics/move_semantics6.rs" }, + { name = "structs1", path = "../exercises/07_structs/structs1.rs" }, + { name = "structs2", path = "../exercises/07_structs/structs2.rs" }, + { name = "structs3", path = "../exercises/07_structs/structs3.rs" }, + { name = "enums1", path = "../exercises/08_enums/enums1.rs" }, + { name = "enums2", path = "../exercises/08_enums/enums2.rs" }, + { name = "enums3", path = "../exercises/08_enums/enums3.rs" }, + { name = "strings1", path = "../exercises/09_strings/strings1.rs" }, + { name = "strings2", path = "../exercises/09_strings/strings2.rs" }, + { name = "strings3", path = "../exercises/09_strings/strings3.rs" }, + { name = "strings4", path = "../exercises/09_strings/strings4.rs" }, + { name = "modules1", path = "../exercises/10_modules/modules1.rs" }, + { name = "modules2", path = "../exercises/10_modules/modules2.rs" }, + { name = "modules3", path = "../exercises/10_modules/modules3.rs" }, + { name = "hashmaps1", path = "../exercises/11_hashmaps/hashmaps1.rs" }, + { name = "hashmaps2", path = "../exercises/11_hashmaps/hashmaps2.rs" }, + { name = "hashmaps3", path = "../exercises/11_hashmaps/hashmaps3.rs" }, + { name = "quiz2", path = "../exercises/quiz2.rs" }, + { name = "options1", path = "../exercises/12_options/options1.rs" }, + { name = "options2", path = "../exercises/12_options/options2.rs" }, + { name = "options3", path = "../exercises/12_options/options3.rs" }, + { name = "errors1", path = "../exercises/13_error_handling/errors1.rs" }, + { name = "errors2", path = "../exercises/13_error_handling/errors2.rs" }, + { name = "errors3", path = "../exercises/13_error_handling/errors3.rs" }, + { name = "errors4", path = "../exercises/13_error_handling/errors4.rs" }, + { name = "errors5", path = "../exercises/13_error_handling/errors5.rs" }, + { name = "errors6", path = "../exercises/13_error_handling/errors6.rs" }, + { name = "generics1", path = "../exercises/14_generics/generics1.rs" }, + { name = "generics2", path = "../exercises/14_generics/generics2.rs" }, + { name = "traits1", path = "../exercises/15_traits/traits1.rs" }, + { name = "traits2", path = "../exercises/15_traits/traits2.rs" }, + { name = "traits3", path = "../exercises/15_traits/traits3.rs" }, + { name = "traits4", path = "../exercises/15_traits/traits4.rs" }, + { name = "traits5", path = "../exercises/15_traits/traits5.rs" }, + { name = "quiz3", path = "../exercises/quiz3.rs" }, + { name = "lifetimes1", path = "../exercises/16_lifetimes/lifetimes1.rs" }, + { name = "lifetimes2", path = "../exercises/16_lifetimes/lifetimes2.rs" }, + { name = "lifetimes3", path = "../exercises/16_lifetimes/lifetimes3.rs" }, + { name = "tests1", path = "../exercises/17_tests/tests1.rs" }, + { name = "tests2", path = "../exercises/17_tests/tests2.rs" }, + { name = "tests3", path = "../exercises/17_tests/tests3.rs" }, + { name = "tests4", path = "../exercises/17_tests/tests4.rs" }, + { name = "iterators1", path = "../exercises/18_iterators/iterators1.rs" }, + { name = "iterators2", path = "../exercises/18_iterators/iterators2.rs" }, + { name = "iterators3", path = "../exercises/18_iterators/iterators3.rs" }, + { name = "iterators4", path = "../exercises/18_iterators/iterators4.rs" }, + { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, + { name = "box1", path = "../exercises/19_smart_pointers/box1.rs" }, + { name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" }, + { name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" }, + { name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" }, + { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, + { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, + { name = "threads3", path = "../exercises/20_threads/threads3.rs" }, + { name = "macros1", path = "../exercises/21_macros/macros1.rs" }, + { name = "macros2", path = "../exercises/21_macros/macros2.rs" }, + { name = "macros3", path = "../exercises/21_macros/macros3.rs" }, + { name = "macros4", path = "../exercises/21_macros/macros4.rs" }, + { name = "clippy1", path = "../exercises/22_clippy/clippy1.rs" }, + { name = "clippy2", path = "../exercises/22_clippy/clippy2.rs" }, + { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, + { name = "using_as", path = "../exercises/23_conversions/using_as.rs" }, + { name = "from_into", path = "../exercises/23_conversions/from_into.rs" }, + { name = "from_str", path = "../exercises/23_conversions/from_str.rs" }, + { name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" }, + { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, +] + +[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs new file mode 100644 index 00000000..20167a1e --- /dev/null +++ b/src/bin/gen-dev-cargo-toml.rs @@ -0,0 +1,56 @@ +use anyhow::{bail, Context, Result}; +use serde::Deserialize; +use std::{ + fs::{self, create_dir}, + io::ErrorKind, +}; + +#[derive(Deserialize)] +struct Exercise { + name: String, + path: String, +} + +#[derive(Deserialize)] +struct InfoToml { + exercises: Vec, +} + +fn main() -> Result<()> { + let exercises = toml_edit::de::from_str::( + &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?, + ) + .context("Failed to deserialize `info.toml`")? + .exercises; + + let mut buf = Vec::with_capacity(1 << 14); + + buf.extend_from_slice(b"bin = [\n"); + + for exercise in exercises { + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise.name.as_bytes()); + buf.extend_from_slice(b"\", path = \"../"); + buf.extend_from_slice(exercise.path.as_bytes()); + buf.extend_from_slice(b"\" },\n"); + } + + buf.extend_from_slice( + br#"] + +[package] +name = "rustlings" +version = "0.0.0" +edition = "2021" +publish = false +"#, + ); + + if let Err(e) = create_dir("dev") { + if e.kind() != ErrorKind::AlreadyExists { + bail!("Failed to create the `dev` directory: {e}"); + } + } + + fs::write("dev/Cargo.toml", buf).context("Failed to write `dev/Cargo.toml`") +} diff --git a/src/exercise.rs b/src/exercise.rs index e7045d60..450acf45 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -91,9 +91,17 @@ pub struct ContextLine { impl Exercise { fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { - Command::new("cargo") - .arg(command) - .arg("--color") + let mut cmd = Command::new("cargo"); + cmd.arg(command); + + // A hack to make `cargo run` work when developing Rustlings. + // Use `dev/Cargo.toml` when in the directory of the repository. + #[cfg(debug_assertions)] + if std::path::Path::new("tests").exists() { + cmd.arg("--manifest-path").arg("dev/Cargo.toml"); + } + + cmd.arg("--color") .arg("always") .arg("-q") .arg("--bin") diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs new file mode 100644 index 00000000..7f1771b6 --- /dev/null +++ b/tests/dev_cargo_bins.rs @@ -0,0 +1,39 @@ +// Makes sure that `dev/Cargo.toml` is synced with `info.toml`. +// When this test fails, you just need to run `cargo run --bin gen-dev-cargo-toml`. + +use serde::Deserialize; +use std::fs; + +#[derive(Deserialize)] +struct Exercise { + name: String, + path: String, +} + +#[derive(Deserialize)] +struct InfoToml { + exercises: Vec, +} + +#[test] +fn dev_cargo_bins() { + let content = fs::read_to_string("exercises/Cargo.toml").unwrap(); + + let exercises = toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) + .unwrap() + .exercises; + + let mut start_ind = 0; + for exercise in exercises { + let name_start = start_ind + content[start_ind..].find('"').unwrap() + 1; + let name_end = name_start + content[name_start..].find('"').unwrap(); + assert_eq!(exercise.name, &content[name_start..name_end]); + + // +3 to skip `../` at the begeinning of the path. + let path_start = name_end + content[name_end + 1..].find('"').unwrap() + 5; + let path_end = path_start + content[path_start..].find('"').unwrap(); + assert_eq!(exercise.path, &content[path_start..path_end]); + + start_ind = path_end + 1; + } +} From 2f30eac27f2b57148081dbe1c489e6c47f01d6a9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 17:36:42 +0200 Subject: [PATCH 028/433] Remove unneeded .iter() --- src/main.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0f298dde..f9e0f830 100644 --- a/src/main.rs +++ b/src/main.rs @@ -339,12 +339,8 @@ fn watch( clear_screen(); - let failed_exercise_hint = match verify( - exercises.iter(), - (0, exercises.len()), - verbose, - success_hints, - ) { + let failed_exercise_hint = match verify(exercises, (0, exercises.len()), verbose, success_hints) + { Ok(_) => return Ok(WatchStatus::Finished), Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), }; From fdd7de00bd37e43a4e464d1cb5cc10c3753b3688 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 18:21:56 +0200 Subject: [PATCH 029/433] Improvements to `verify` --- src/main.rs | 37 ++++++++++++++++--------------------- src/verify.rs | 17 ++++++----------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index f9e0f830..7b7b1655 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ use console::Emoji; use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; -use std::ffi::OsStr; use std::io::{BufRead, Write}; use std::path::Path; use std::process::{exit, Command}; @@ -344,44 +343,40 @@ fn watch( Ok(_) => return Ok(WatchStatus::Finished), Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), }; + spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); + + let mut pending_exercises = Vec::with_capacity(exercises.len()); loop { match rx.recv_timeout(Duration::from_secs(1)) { Ok(event) => match event { Ok(events) => { for event in events { - let event_path = event.path; if event.kind == DebouncedEventKind::Any - && event_path.extension() == Some(OsStr::new("rs")) - && event_path.exists() + && event.path.extension().is_some_and(|ext| ext == "rs") { - let filepath = event_path.as_path().canonicalize().unwrap(); - // TODO: Remove unwrap - let pending_exercises = exercises - .iter() - .find(|e| filepath.ends_with(&e.path)) - .into_iter() - .chain(exercises.iter().filter(|e| { - !e.looks_done().unwrap() && !filepath.ends_with(&e.path) - })); - let num_done = exercises - .iter() - .filter(|e| e.looks_done().unwrap() && !filepath.ends_with(&e.path)) - .count(); + pending_exercises.extend(exercises.iter().filter(|exercise| { + !exercise.looks_done().unwrap_or(false) + || event.path.ends_with(&exercise.path) + })); + let num_done = exercises.len() - pending_exercises.len(); + clear_screen(); + match verify( - pending_exercises, + pending_exercises.iter().copied(), (num_done, exercises.len()), verbose, success_hints, ) { Ok(_) => return Ok(WatchStatus::Finished), Err(exercise) => { - let mut failed_exercise_hint = - failed_exercise_hint.lock().unwrap(); - *failed_exercise_hint = Some(exercise.hint.clone()); + let hint = exercise.hint.clone(); + *failed_exercise_hint.lock().unwrap() = Some(hint); } } + + pending_exercises.clear(); } } } diff --git a/src/verify.rs b/src/verify.rs index adfd3b26..6e048a19 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -16,7 +16,7 @@ use crate::exercise::{Exercise, Mode, State}; // If the Exercise being verified is a test, the verbose boolean // determines whether or not the test harness outputs are displayed. pub fn verify<'a>( - exercises: impl IntoIterator, + pending_exercises: impl IntoIterator, progress: (usize, usize), verbose: bool, success_hints: bool, @@ -33,7 +33,7 @@ pub fn verify<'a>( bar.set_position(num_done as u64); bar.set_message(format!("({percentage:.1} %)")); - for exercise in exercises { + for exercise in pending_exercises { let compile_result = match exercise.mode { Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), Mode::Compile => compile_and_run_interactively(exercise, success_hints), @@ -45,16 +45,11 @@ pub fn verify<'a>( percentage += 100.0 / total as f32; bar.inc(1); bar.set_message(format!("({percentage:.1} %)")); - if bar.position() == total as u64 { - println!( - "Progress: You completed {} / {} exercises ({:.1} %).", - bar.position(), - total, - percentage - ); - bar.finish(); - } } + + bar.finish(); + println!("You completed all exercises!"); + Ok(()) } From def8d2c569a8a637396960c8513a0b1bdf88ef0c Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 18:38:01 +0200 Subject: [PATCH 030/433] Add VerifyState --- src/main.rs | 34 ++++++++++++++++------------------ src/verify.rs | 19 ++++++++++++------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7b7b1655..c8c65848 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; use crate::run::run; use crate::verify::verify; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use console::Emoji; -use notify_debouncer_mini::notify::{self, RecursiveMode}; +use notify_debouncer_mini::notify::RecursiveMode; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use shlex::Shlex; use std::io::{BufRead, Write}; @@ -16,6 +16,7 @@ use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{io, thread}; +use verify::VerifyState; #[macro_use] mod ui; @@ -218,9 +219,10 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini println!("{}", exercise.hint); } - Subcommands::Verify => { - verify(&exercises, (0, exercises.len()), verbose, false).unwrap_or_else(|_| exit(1)); - } + Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? { + VerifyState::AllExercisesDone => println!("All exercises done!"), + VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), + }, Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { Err(e) => { @@ -317,11 +319,7 @@ enum WatchStatus { Unfinished, } -fn watch( - exercises: &[Exercise], - verbose: bool, - success_hints: bool, -) -> notify::Result { +fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result { /* Clears the terminal with an ANSI escape code. Works in UNIX and newer Windows terminals. */ fn clear_screen() { @@ -338,11 +336,11 @@ fn watch( clear_screen(); - let failed_exercise_hint = match verify(exercises, (0, exercises.len()), verbose, success_hints) - { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), - }; + let failed_exercise_hint = + match verify(exercises, (0, exercises.len()), verbose, success_hints)? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), + }; spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); @@ -368,9 +366,9 @@ fn watch( (num_done, exercises.len()), verbose, success_hints, - ) { - Ok(_) => return Ok(WatchStatus::Finished), - Err(exercise) => { + )? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => { let hint = exercise.hint.clone(); *failed_exercise_hint.lock().unwrap() = Some(hint); } diff --git a/src/verify.rs b/src/verify.rs index 6e048a19..02bff995 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -10,6 +10,11 @@ use std::{ use crate::exercise::{Exercise, Mode, State}; +pub enum VerifyState<'a> { + AllExercisesDone, + Failed(&'a Exercise), +} + // Verify that the provided container of Exercise objects // can be compiled and run without any failures. // Any such failures will be reported to the end user. @@ -20,7 +25,7 @@ pub fn verify<'a>( progress: (usize, usize), verbose: bool, success_hints: bool, -) -> Result<(), &'a Exercise> { +) -> Result> { let (num_done, total) = progress; let bar = ProgressBar::new(total as u64); let mut percentage = num_done as f32 / total as f32 * 100.0; @@ -35,12 +40,12 @@ pub fn verify<'a>( for exercise in pending_exercises { let compile_result = match exercise.mode { - Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), - Mode::Compile => compile_and_run_interactively(exercise, success_hints), - Mode::Clippy => compile_only(exercise, success_hints), + Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?, + Mode::Compile => compile_and_run_interactively(exercise, success_hints)?, + Mode::Clippy => compile_only(exercise, success_hints)?, }; - if !compile_result.unwrap_or(false) { - return Err(exercise); + if !compile_result { + return Ok(VerifyState::Failed(exercise)); } percentage += 100.0 / total as f32; bar.inc(1); @@ -50,7 +55,7 @@ pub fn verify<'a>( bar.finish(); println!("You completed all exercises!"); - Ok(()) + Ok(VerifyState::AllExercisesDone) } #[derive(PartialEq, Eq)] From 190945352a2316154d9856a5d882893326e0136a Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Apr 2024 18:52:43 +0200 Subject: [PATCH 031/433] Add comments about dev/Cargo.toml --- dev/Cargo.toml | 3 +++ src/bin/gen-dev-cargo-toml.rs | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 4ad48866..e4e7be78 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,3 +1,6 @@ +# This file is a hack to allow using `cargo r` to test `rustlings` during development. +# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`. + bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs index 20167a1e..65cc2447 100644 --- a/src/bin/gen-dev-cargo-toml.rs +++ b/src/bin/gen-dev-cargo-toml.rs @@ -1,3 +1,7 @@ +// Generates `dev/Cargo.toml` such that it is synced with `info.toml`. +// `dev/Cargo.toml` is a hack to allow using `cargo r` to test `rustlings` +// during development. + use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{ @@ -25,7 +29,12 @@ fn main() -> Result<()> { let mut buf = Vec::with_capacity(1 << 14); - buf.extend_from_slice(b"bin = [\n"); + buf.extend_from_slice( + b"# This file is a hack to allow using `cargo r` to test `rustlings` during development. +# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`. + +bin = [\n", + ); for exercise in exercises { buf.extend_from_slice(b" { name = \""); From 569a68eb73b82040588138b0ba1daabca1a7d415 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 15:44:48 +0200 Subject: [PATCH 032/433] Minify generated Cargo.toml --- src/init.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/init.rs b/src/init.rs index d958c96e..b52b6139 100644 --- a/src/init.rs +++ b/src/init.rs @@ -10,21 +10,25 @@ use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); + cargo_toml.extend_from_slice(b"bin = [\n"); + for exercise in exercises { + cargo_toml.extend_from_slice(b" { name = \""); + cargo_toml.extend_from_slice(exercise.name.as_bytes()); + cargo_toml.extend_from_slice(b"\", path = \""); + cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); + cargo_toml.extend_from_slice(b"\" },\n"); + } + cargo_toml.extend_from_slice( - br#"[package] + br#"] + +[package] name = "rustlings" version = "0.0.0" edition = "2021" publish = false "#, ); - for exercise in exercises { - cargo_toml.extend_from_slice(b"\n[[bin]]\nname = \""); - cargo_toml.extend_from_slice(exercise.name.as_bytes()); - cargo_toml.extend_from_slice(b"\"\npath = \""); - cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); - cargo_toml.extend_from_slice(b"\"\n"); - } OpenOptions::new() .create_new(true) .write(true) From b6c434c445d91a9e886e5639b078635e5eca4eb3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 15:45:53 +0200 Subject: [PATCH 033/433] Remove optional version field --- dev/Cargo.toml | 1 - src/bin/gen-dev-cargo-toml.rs | 1 - src/init.rs | 1 - tests/fixture/failure/Cargo.toml | 1 - tests/fixture/state/Cargo.toml | 1 - tests/fixture/success/Cargo.toml | 1 - 6 files changed, 6 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index e4e7be78..7868b97c 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -102,6 +102,5 @@ bin = [ [package] name = "rustlings" -version = "0.0.0" edition = "2021" publish = false diff --git a/src/bin/gen-dev-cargo-toml.rs b/src/bin/gen-dev-cargo-toml.rs index 65cc2447..ff8f31db 100644 --- a/src/bin/gen-dev-cargo-toml.rs +++ b/src/bin/gen-dev-cargo-toml.rs @@ -49,7 +49,6 @@ bin = [\n", [package] name = "rustlings" -version = "0.0.0" edition = "2021" publish = false "#, diff --git a/src/init.rs b/src/init.rs index b52b6139..6af32351 100644 --- a/src/init.rs +++ b/src/init.rs @@ -24,7 +24,6 @@ fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { [package] name = "rustlings" -version = "0.0.0" edition = "2021" publish = false "#, diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml index dd728c34..e111cf2b 100644 --- a/tests/fixture/failure/Cargo.toml +++ b/tests/fixture/failure/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "tests" -version = "0.0.0" edition = "2021" publish = false diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml index 5cfa42ba..c8d74e47 100644 --- a/tests/fixture/state/Cargo.toml +++ b/tests/fixture/state/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "tests" -version = "0.0.0" edition = "2021" publish = false diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml index c0059284..f26a44f1 100644 --- a/tests/fixture/success/Cargo.toml +++ b/tests/fixture/success/Cargo.toml @@ -1,6 +1,5 @@ [package] name = "tests" -version = "0.0.0" edition = "2021" publish = false From 2b6f9fb6a7a33f074aa609b2da1ac084bc3ecd6b Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 20:21:55 +0200 Subject: [PATCH 034/433] Add Ratatui --- Cargo.lock | 323 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + 2 files changed, 314 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8e5b723..38f8170c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,6 +23,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "anstream" version = "0.6.13" @@ -109,6 +127,21 @@ dependencies = [ "serde", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -143,10 +176,10 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -161,6 +194,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.8" @@ -189,6 +235,31 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "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 = "difflib" version = "0.4.0" @@ -270,6 +341,16 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -309,6 +390,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "inotify" version = "0.9.6" @@ -338,6 +425,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -382,6 +478,16 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" @@ -389,10 +495,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] -name = "memchr" -version = "2.7.1" +name = "lru" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mio" @@ -457,6 +572,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "portable-atomic" version = "1.6.0" @@ -511,6 +661,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ratatui" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "itertools", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -570,10 +740,12 @@ dependencies = [ "assert_cmd", "clap", "console", + "crossterm", "glob", "indicatif", "notify-debouncer-mini", "predicates", + "ratatui", "rustlings-macros", "serde", "serde_json", @@ -590,6 +762,12 @@ dependencies = [ "quote", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.17" @@ -605,6 +783,12 @@ 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.197" @@ -622,7 +806,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -652,16 +836,101 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "strsim" -version = "0.11.0" +name = "signal-hook" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +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 = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stability" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.58", +] [[package]] name = "syn" -version = "2.0.55" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -702,6 +971,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.11" @@ -714,6 +989,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -928,3 +1209,23 @@ name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] diff --git a/Cargo.toml b/Cargo.toml index 86187b4b..2a22fce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,10 @@ edition.workspace = true anyhow = "1.0.81" clap = { version = "4.5.4", features = ["derive"] } console = "0.15.8" +crossterm = "0.27.0" indicatif = "0.17.8" notify-debouncer-mini = "0.4.1" +ratatui = "0.26.1" rustlings-macros = { path = "rustlings-macros" } serde_json = "1.0.115" serde = { version = "1.0.197", features = ["derive"] } From 9ea744a7104f441ef505db0a96e852f93d8c0bf4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 20:27:30 +0200 Subject: [PATCH 035/433] Remove deps not needed in the TUI --- Cargo.lock | 42 ------------------------------------------ Cargo.toml | 2 -- 2 files changed, 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38f8170c..4aaec38c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,19 +377,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indicatif" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - [[package]] name = "indoc" version = "2.0.5" @@ -416,15 +403,6 @@ dependencies = [ "libc", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "itertools" version = "0.12.1" @@ -566,12 +544,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.19.0" @@ -607,12 +579,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" -[[package]] -name = "portable-atomic" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" - [[package]] name = "predicates" version = "3.1.0" @@ -742,14 +708,12 @@ dependencies = [ "console", "crossterm", "glob", - "indicatif", "notify-debouncer-mini", "predicates", "ratatui", "rustlings-macros", "serde", "serde_json", - "shlex", "toml_edit", "which", "winnow", @@ -829,12 +793,6 @@ dependencies = [ "serde", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook" version = "0.3.17" diff --git a/Cargo.toml b/Cargo.toml index 2a22fce1..3c187417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,11 @@ anyhow = "1.0.81" clap = { version = "4.5.4", features = ["derive"] } console = "0.15.8" crossterm = "0.27.0" -indicatif = "0.17.8" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" rustlings-macros = { path = "rustlings-macros" } serde_json = "1.0.115" serde = { version = "1.0.197", features = ["derive"] } -shlex = "1.3.0" toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } which = "6.0.1" winnow = "0.6.5" From 34375b2ebfbdb0b6504a56c82635c8c9d3d6ce59 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 21:06:11 +0200 Subject: [PATCH 036/433] Clean up as a preparation for the TUI --- src/main.rs | 44 ++------- src/run.rs | 40 +++----- src/verify.rs | 249 +++++++++++--------------------------------------- 3 files changed, 77 insertions(+), 256 deletions(-) diff --git a/src/main.rs b/src/main.rs index c8c65848..20ec290f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,9 @@ use clap::{Parser, Subcommand}; use console::Emoji; use notify_debouncer_mini::notify::RecursiveMode; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; -use shlex::Shlex; use std::io::{BufRead, Write}; use std::path::Path; -use std::process::{exit, Command}; +use std::process::exit; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::{Arc, Mutex}; @@ -31,9 +30,6 @@ mod verify; #[derive(Parser)] #[command(version)] struct Args { - /// Show outputs from the test exercises - #[arg(long)] - nocapture: bool, #[command(subcommand)] command: Option, } @@ -45,11 +41,7 @@ enum Subcommands { /// Verify all exercises according to the recommended order Verify, /// Rerun `verify` when files were edited - Watch { - /// Show hints on success - #[arg(long)] - success_hints: bool, - }, + Watch, /// Run/Test a single exercise Run { /// The name of the exercise @@ -117,7 +109,6 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let verbose = args.nocapture; let command = args.command.unwrap_or_else(|| { println!("{DEFAULT_OUT}\n"); exit(0); @@ -203,7 +194,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Subcommands::Run { name } => { let exercise = find_exercise(&name, &exercises)?; - run(exercise, verbose).unwrap_or_else(|_| exit(1)); + run(exercise).unwrap_or_else(|_| exit(1)); } Subcommands::Reset { name } => { @@ -219,12 +210,12 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini println!("{}", exercise.hint); } - Subcommands::Verify => match verify(&exercises, (0, exercises.len()), verbose, false)? { + Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, - Subcommands::Watch { success_hints } => match watch(&exercises, verbose, success_hints) { + Subcommands::Watch => match watch(&exercises) { Err(e) => { println!("Error: Could not watch your progress. Error message was {e:?}."); println!("Most likely you've run out of disk space or your 'inotify limit' has been reached."); @@ -277,17 +268,6 @@ fn spawn_watch_shell( println!("Bye!"); } else if input == "help" { println!("{WATCH_MODE_HELP_MESSAGE}"); - } else if let Some(cmd) = input.strip_prefix('!') { - let mut parts = Shlex::new(cmd); - - let Some(program) = parts.next() else { - println!("no command provided"); - continue; - }; - - if let Err(e) = Command::new(program).args(parts).status() { - println!("failed to execute command `{cmd}`: {e}"); - } } else { println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}"); } @@ -319,7 +299,7 @@ enum WatchStatus { Unfinished, } -fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result { +fn watch(exercises: &[Exercise]) -> Result { /* Clears the terminal with an ANSI escape code. Works in UNIX and newer Windows terminals. */ fn clear_screen() { @@ -336,11 +316,10 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result return Ok(WatchStatus::Finished), - VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), - }; + let failed_exercise_hint = match verify(exercises, (0, exercises.len()))? { + VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), + VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), + }; spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); @@ -364,8 +343,6 @@ fn watch(exercises: &[Exercise], verbose: bool, success_hints: bool) -> Result return Ok(WatchStatus::Finished), VerifyState::Failed(exercise) => { @@ -429,7 +406,6 @@ const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: hint - prints the current exercise's hint clear - clears the screen quit - quits watch mode - ! - executes a command, like `!rustc --explain E0381` help - displays this help message Watch mode automatically re-evaluates the current exercise diff --git a/src/run.rs b/src/run.rs index 3f93f146..0a09eccf 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,39 +1,27 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use std::io::{stdout, Write}; -use std::time::Duration; -use crate::exercise::{Exercise, Mode}; -use crate::verify::test; -use indicatif::ProgressBar; +use crate::exercise::Exercise; // Invoke the rust compiler on the path of the given exercise, // and run the ensuing binary. // The verbose argument helps determine whether or not to show // the output from the test harnesses (if the mode of the exercise is test) -pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> { - match exercise.mode { - Mode::Test => test(exercise, verbose), - Mode::Compile | Mode::Clippy => compile_and_run(exercise), - } -} - -// Compile and run an exercise. -// This is strictly for non-test binaries, so output is displayed -fn compile_and_run(exercise: &Exercise) -> Result<()> { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Running {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - +pub fn run(exercise: &Exercise) -> Result<()> { let output = exercise.run()?; - progress_bar.finish_and_clear(); - stdout().write_all(&output.stdout)?; - if !output.status.success() { - stdout().write_all(&output.stderr)?; - warn!("Ran {} with errors", exercise); - bail!("TODO"); + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; + } + + if output.status.success() { + success!("Successfully ran {}", exercise); + } else { + warn!("Ran {} with errors", exercise); } - success!("Successfully ran {}", exercise); Ok(()) } diff --git a/src/verify.rs b/src/verify.rs index ef966f60..5b053940 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,12 +1,6 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use console::style; -use indicatif::{ProgressBar, ProgressStyle}; -use std::{ - env, - io::{stdout, Write}, - process::Output, - time::Duration, -}; +use std::io::{stdout, Write}; use crate::exercise::{Exercise, Mode, State}; @@ -23,201 +17,64 @@ pub enum VerifyState<'a> { pub fn verify<'a>( pending_exercises: impl IntoIterator, progress: (usize, usize), - verbose: bool, - success_hints: bool, ) -> Result> { - let (num_done, total) = progress; - let bar = ProgressBar::new(total as u64); - let mut percentage = num_done as f32 / total as f32 * 100.0; - bar.set_style( - ProgressStyle::default_bar() - .template("Progress: [{bar:60.green/red}] {pos}/{len} {msg}") - .expect("Progressbar template should be valid!") - .progress_chars("#>-"), + let (mut num_done, total) = progress; + println!( + "Progress: {num_done}/{total} ({:.1}%)\n", + num_done as f32 / total as f32 * 100.0, ); - bar.set_position(num_done as u64); - bar.set_message(format!("({percentage:.1} %)")); for exercise in pending_exercises { - let compile_result = match exercise.mode { - Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints)?, - Mode::Compile => compile_and_run_interactively(exercise, success_hints)?, - Mode::Clippy => compile_only(exercise, success_hints)?, - }; - if !compile_result { + let output = exercise.run()?; + + { + let mut stdout = stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; + } + + if !output.status.success() { return Ok(VerifyState::Failed(exercise)); } - percentage += 100.0 / total as f32; - bar.inc(1); - bar.set_message(format!("({percentage:.1} %)")); - } - bar.finish(); - println!("You completed all exercises!"); + println!(); + match exercise.mode { + Mode::Compile => success!("Successfully ran {}!", exercise), + Mode::Test => success!("Successfully tested {}!", exercise), + Mode::Clippy => success!("Successfully checked {}!", exercise), + } + + if let State::Pending(context) = exercise.state()? { + println!( + "\nYou can keep working on this exercise, +or jump into the next one by removing the {} comment:\n", + style("`I AM NOT DONE`").bold() + ); + + for context_line in context { + let formatted_line = if context_line.important { + format!("{}", style(context_line.line).bold()) + } else { + context_line.line + }; + + println!( + "{:>2} {} {}", + style(context_line.number).blue().bold(), + style("|").blue(), + formatted_line, + ); + } + return Ok(VerifyState::Failed(exercise)); + } + + num_done += 1; + println!( + "Progress: {num_done}/{total} ({:.1}%)\n", + num_done as f32 / total as f32 * 100.0, + ); + } Ok(VerifyState::AllExercisesDone) } - -#[derive(PartialEq, Eq)] -enum RunMode { - Interactive, - NonInteractive, -} - -// Compile and run the resulting test harness of the given Exercise -pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> { - compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?; - Ok(()) -} - -// Invoke the rust compiler without running the resulting binary -fn compile_only(exercise: &Exercise, success_hints: bool) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Compiling {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let _ = exercise.run()?; - progress_bar.finish_and_clear(); - - prompt_for_completion(exercise, None, success_hints) -} - -// Compile the given Exercise and run the resulting binary in an interactive mode -fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Running {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let output = exercise.run()?; - progress_bar.finish_and_clear(); - - if !output.status.success() { - warn!("Ran {} with errors", exercise); - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } - bail!("TODO"); - } - - prompt_for_completion(exercise, Some(output), success_hints) -} - -// Compile the given Exercise as a test harness and display -// the output if verbose is set to true -fn compile_and_test( - exercise: &Exercise, - run_mode: RunMode, - verbose: bool, - success_hints: bool, -) -> Result { - let progress_bar = ProgressBar::new_spinner(); - progress_bar.set_message(format!("Testing {exercise}...")); - progress_bar.enable_steady_tick(Duration::from_millis(100)); - - let output = exercise.run()?; - progress_bar.finish_and_clear(); - - if !output.status.success() { - warn!( - "Testing of {} failed! Please try again. Here's the output:", - exercise - ); - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } - bail!("TODO"); - } - - if verbose { - stdout().write_all(&output.stdout)?; - } - - if run_mode == RunMode::Interactive { - prompt_for_completion(exercise, None, success_hints) - } else { - Ok(true) - } -} - -fn prompt_for_completion( - exercise: &Exercise, - prompt_output: Option, - success_hints: bool, -) -> Result { - let context = match exercise.state()? { - State::Done => return Ok(true), - State::Pending(context) => context, - }; - match exercise.mode { - Mode::Compile => success!("Successfully ran {}!", exercise), - Mode::Test => success!("Successfully tested {}!", exercise), - Mode::Clippy => success!("Successfully compiled {}!", exercise), - } - - let no_emoji = env::var("NO_EMOJI").is_ok(); - - let clippy_success_msg = if no_emoji { - "The code is compiling, and Clippy is happy!" - } else { - "The code is compiling, and 📎 Clippy 📎 is happy!" - }; - - let success_msg = match exercise.mode { - Mode::Compile => "The code is compiling!", - Mode::Test => "The code is compiling, and the tests pass!", - Mode::Clippy => clippy_success_msg, - }; - - if no_emoji { - println!("\n~*~ {success_msg} ~*~\n"); - } else { - println!("\n🎉 🎉 {success_msg} 🎉 🎉\n"); - } - - if let Some(output) = prompt_output { - let separator = separator(); - println!("Output:\n{separator}"); - stdout().write_all(&output.stdout).unwrap(); - println!("\n{separator}\n"); - } - if success_hints { - println!( - "Hints:\n{separator}\n{}\n{separator}\n", - exercise.hint, - separator = separator(), - ); - } - - println!("You can keep working on this exercise,"); - println!( - "or jump into the next one by removing the {} comment:", - style("`I AM NOT DONE`").bold() - ); - println!(); - for context_line in context { - let formatted_line = if context_line.important { - format!("{}", style(context_line.line).bold()) - } else { - context_line.line - }; - - println!( - "{:>2} {} {}", - style(context_line.number).blue().bold(), - style("|").blue(), - formatted_line, - ); - } - - Ok(false) -} - -fn separator() -> console::StyledObject<&'static str> { - style("====================").bold() -} From 445441ce25ec8658bcdec6b2038d17e893a5903f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 4 Apr 2024 23:16:57 +0200 Subject: [PATCH 037/433] Make gen-dev-cargo-toml a separate package so that `cargo install` only installs `rustlings` --- Cargo.lock | 9 +++++++++ Cargo.toml | 14 +++++++++++--- dev/Cargo.toml | 4 ++-- gen-dev-cargo-toml/Cargo.toml | 10 ++++++++++ .../src/main.rs | 6 +++--- tests/dev_cargo_bins.rs | 2 +- 6 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 gen-dev-cargo-toml/Cargo.toml rename src/bin/gen-dev-cargo-toml.rs => gen-dev-cargo-toml/src/main.rs (86%) diff --git a/Cargo.lock b/Cargo.lock index 4aaec38c..e03980ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,6 +330,15 @@ dependencies = [ "libc", ] +[[package]] +name = "gen-dev-cargo-toml" +version = "0.0.0" +dependencies = [ + "anyhow", + "serde", + "toml_edit", +] + [[package]] name = "glob" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 3c187417..d80550a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,9 @@ exclude = [ "tests/fixture/success", "dev", ] +members = [ + "gen-dev-cargo-toml", +] [workspace.package] version = "6.0.0" @@ -16,6 +19,11 @@ authors = [ license = "MIT" edition = "2021" +[workspace.dependencies] +anyhow = "1.0.81" +serde = { version = "1.0.197", features = ["derive"] } +toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } + [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" @@ -26,7 +34,7 @@ license.workspace = true edition.workspace = true [dependencies] -anyhow = "1.0.81" +anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } console = "0.15.8" crossterm = "0.27.0" @@ -34,8 +42,8 @@ notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" rustlings-macros = { path = "rustlings-macros" } serde_json = "1.0.115" -serde = { version = "1.0.197", features = ["derive"] } -toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } +serde.workspace = true +toml_edit.workspace = true which = "6.0.1" winnow = "0.6.5" diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 7868b97c..ed9b3ed3 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,5 +1,5 @@ -# This file is a hack to allow using `cargo r` to test `rustlings` during development. -# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`. +# This file is a hack to allow using `cargo run` to test `rustlings` during development. +# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`. bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, diff --git a/gen-dev-cargo-toml/Cargo.toml b/gen-dev-cargo-toml/Cargo.toml new file mode 100644 index 00000000..8922ae8c --- /dev/null +++ b/gen-dev-cargo-toml/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gen-dev-cargo-toml" +publish = false +license.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +serde.workspace = true +toml_edit.workspace = true diff --git a/src/bin/gen-dev-cargo-toml.rs b/gen-dev-cargo-toml/src/main.rs similarity index 86% rename from src/bin/gen-dev-cargo-toml.rs rename to gen-dev-cargo-toml/src/main.rs index ff8f31db..622762ad 100644 --- a/src/bin/gen-dev-cargo-toml.rs +++ b/gen-dev-cargo-toml/src/main.rs @@ -1,5 +1,5 @@ // Generates `dev/Cargo.toml` such that it is synced with `info.toml`. -// `dev/Cargo.toml` is a hack to allow using `cargo r` to test `rustlings` +// `dev/Cargo.toml` is a hack to allow using `cargo run` to test `rustlings` // during development. use anyhow::{bail, Context, Result}; @@ -30,8 +30,8 @@ fn main() -> Result<()> { let mut buf = Vec::with_capacity(1 << 14); buf.extend_from_slice( - b"# This file is a hack to allow using `cargo r` to test `rustlings` during development. -# You shouldn't edit it manually. It is created and updated by running `cargo run --bin gen-dev-cargo-toml`. + b"# This file is a hack to allow using `cargo run` to test `rustlings` during development. +# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`. bin = [\n", ); diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs index 7f1771b6..ad4832f8 100644 --- a/tests/dev_cargo_bins.rs +++ b/tests/dev_cargo_bins.rs @@ -1,5 +1,5 @@ // Makes sure that `dev/Cargo.toml` is synced with `info.toml`. -// When this test fails, you just need to run `cargo run --bin gen-dev-cargo-toml`. +// When this test fails, you just need to run `cargo run -p gen-dev-cargo-toml`. use serde::Deserialize; use std::fs; From 919ba88413fcc495ebde288960079f6f627eb5b7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 00:43:36 +0200 Subject: [PATCH 038/433] Use the pretty format when testing even with -q --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 450acf45..d5ca254e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -114,7 +114,7 @@ impl Exercise { pub fn run(&self) -> Result { match self.mode { Mode::Compile => self.cargo_cmd("run", &[]), - Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]), + Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]), Mode::Clippy => self.cargo_cmd( "clippy", &["--", "-D", "warnings", "-D", "clippy::float_cmp"], From 5a233398ebe7078767404bd05ca06e08b37fb3d4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 00:44:43 +0200 Subject: [PATCH 039/433] Fix tests --- src/run.rs | 10 +++++----- tests/dev_cargo_bins.rs | 2 +- tests/integration_tests.rs | 13 +------------ 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/run.rs b/src/run.rs index 0a09eccf..ee2d3b4f 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{bail, Result}; use std::io::{stdout, Write}; use crate::exercise::Exercise; @@ -17,11 +17,11 @@ pub fn run(exercise: &Exercise) -> Result<()> { stdout.flush()?; } - if output.status.success() { - success!("Successfully ran {}", exercise); - } else { - warn!("Ran {} with errors", exercise); + if !output.status.success() { + bail!("Ran {exercise} with errors"); } + success!("Successfully ran {}", exercise); + Ok(()) } diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs index ad4832f8..c3faea92 100644 --- a/tests/dev_cargo_bins.rs +++ b/tests/dev_cargo_bins.rs @@ -17,7 +17,7 @@ struct InfoToml { #[test] fn dev_cargo_bins() { - let content = fs::read_to_string("exercises/Cargo.toml").unwrap(); + let content = fs::read_to_string("dev/Cargo.toml").unwrap(); let exercises = toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) .unwrap() diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d1694a39..d853521f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -194,24 +194,13 @@ fn run_test_exercise_does_not_prompt() { #[test] fn run_single_test_success_with_output() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["--nocapture", "run", "testSuccess"]) - .current_dir("tests/fixture/success/") - .assert() - .code(0) - .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS")); -} - -#[test] -fn run_single_test_success_without_output() { Command::cargo_bin("rustlings") .unwrap() .args(["run", "testSuccess"]) .current_dir("tests/fixture/success/") .assert() .code(0) - .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS").not()); + .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS")); } #[test] From 157fe016e5f335e04b4dd322623d35a244faa2ab Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 00:49:22 +0200 Subject: [PATCH 040/433] Remove ui.rs --- src/main.rs | 3 --- src/run.rs | 3 ++- src/ui.rs | 28 ---------------------------- src/verify.rs | 7 ++++--- 4 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 src/ui.rs diff --git a/src/main.rs b/src/main.rs index 20ec290f..c62837d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,9 +17,6 @@ use std::time::Duration; use std::{io, thread}; use verify::VerifyState; -#[macro_use] -mod ui; - mod embedded; mod exercise; mod init; diff --git a/src/run.rs b/src/run.rs index ee2d3b4f..38f4e0e2 100644 --- a/src/run.rs +++ b/src/run.rs @@ -21,7 +21,8 @@ pub fn run(exercise: &Exercise) -> Result<()> { bail!("Ran {exercise} with errors"); } - success!("Successfully ran {}", exercise); + // TODO: Color + println!("Successfully ran {exercise}"); Ok(()) } diff --git a/src/ui.rs b/src/ui.rs deleted file mode 100644 index 22d60d96..00000000 --- a/src/ui.rs +++ /dev/null @@ -1,28 +0,0 @@ -macro_rules! print_emoji { - ($emoji:expr, $sign:expr, $color: ident, $fmt:literal, $ex:expr) => {{ - use console::{style, Emoji}; - use std::env; - let formatstr = format!($fmt, $ex); - if env::var_os("NO_EMOJI").is_some() { - println!("{} {}", style($sign).$color(), style(formatstr).$color()); - } else { - println!( - "{} {}", - style(Emoji($emoji, $sign)).$color(), - style(formatstr).$color() - ); - } - }}; -} - -macro_rules! warn { - ($fmt:literal, $ex:expr) => {{ - print_emoji!("⚠️ ", "!", red, $fmt, $ex); - }}; -} - -macro_rules! success { - ($fmt:literal, $ex:expr) => {{ - print_emoji!("✅ ", "✓", green, $fmt, $ex); - }}; -} diff --git a/src/verify.rs b/src/verify.rs index 5b053940..5beb2069 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -39,10 +39,11 @@ pub fn verify<'a>( } println!(); + // TODO: Color match exercise.mode { - Mode::Compile => success!("Successfully ran {}!", exercise), - Mode::Test => success!("Successfully tested {}!", exercise), - Mode::Clippy => success!("Successfully checked {}!", exercise), + Mode::Compile => println!("Successfully ran {exercise}!"), + Mode::Test => println!("Successfully tested {exercise}!"), + Mode::Clippy => println!("Successfully checked {exercise}!"), } if let State::Pending(context) = exercise.state()? { From 1d2c2cffd2f5a85714c3902bec6e8b198fede12f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 00:59:13 +0200 Subject: [PATCH 041/433] Remove .gitattributes --- .gitattributes | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index efdba876..00000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -* text=auto -*.sh text eol=lf From 0bf51c6a0de117d7f28ddf4a253bfc0306f2e78b Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 00:59:21 +0200 Subject: [PATCH 042/433] Ignore .ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0bbbc542..0ea1fb6d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ target/ *.o public/ .direnv/ +.ignore # Local Netlify folder .netlify From b0f19fd862d659d2d4b01f2faa6b006fe2c60561 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 03:04:53 +0200 Subject: [PATCH 043/433] Start with the TUI --- Cargo.lock | 26 ------ Cargo.toml | 1 - src/consts.rs | 59 ++++++++++++ src/main.rs | 245 ++++---------------------------------------------- src/tui.rs | 92 +++++++++++++++++++ src/verify.rs | 16 ++-- 6 files changed, 180 insertions(+), 259 deletions(-) create mode 100644 src/consts.rs create mode 100644 src/tui.rs diff --git a/Cargo.lock b/Cargo.lock index e03980ca..33d3030a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,19 +207,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys 0.52.0", -] - [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -278,12 +265,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "equivalent" version = "1.0.1" @@ -447,12 +428,6 @@ dependencies = [ "libc", ] -[[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.153" @@ -714,7 +689,6 @@ dependencies = [ "anyhow", "assert_cmd", "clap", - "console", "crossterm", "glob", "notify-debouncer-mini", diff --git a/Cargo.toml b/Cargo.toml index d80550a0..da09ba18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,6 @@ edition.workspace = true [dependencies] anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } -console = "0.15.8" crossterm = "0.27.0" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 00000000..40bf150f --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,59 @@ +pub const WELCOME: &str = r" welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/"; + +pub const DEFAULT_OUT: &str = + "Is this your first time? Don't worry, Rustlings was made for beginners! We are +going to teach you a lot of things about Rust, but before we can get +started, here's a couple of notes about how Rustlings operates: + +1. The central concept behind Rustlings is that you solve exercises. These + exercises usually have some sort of syntax error in them, which will cause + them to fail compilation or testing. Sometimes there's a logic error instead + of a syntax error. No matter what error, it's your job to find it and fix it! + You'll know when you fixed it because then, the exercise will compile and + Rustlings will be able to move on to the next exercise. +2. If you run Rustlings in watch mode (which we recommend), it'll automatically + start with the first exercise. Don't get confused by an error message popping + up as soon as you run Rustlings! This is part of the exercise that you're + supposed to solve, so open the exercise file in an editor and start your + detective work! +3. If you're stuck on an exercise, there is a helpful hint you can view by typing + 'hint' (in watch mode), or running `rustlings hint exercise_name`. +4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! + (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, + and sometimes, other learners do too so you can help each other out! + +Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the `rustlings` directory!"; + +pub const FENISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the Fe-nish line! | ++-------------------------- ------------------------+ + \\/\x1b[31m + ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ + ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ + ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ + ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ + ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m + +We hope you enjoyed learning about the various aspects of Rust! +If you noticed any issues, please don't hesitate to report them to our repo. +You can also contribute your own exercises to help the greater community! + +Before reporting an issue or contributing, please read our guidelines: +https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; diff --git a/src/main.rs b/src/main.rs index c62837d3..47afd019 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,22 @@ +use crate::consts::{DEFAULT_OUT, WELCOME}; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; use crate::run::run; +use crate::tui::tui; use crate::verify::verify; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; -use console::Emoji; -use notify_debouncer_mini::notify::RecursiveMode; -use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; -use std::io::{BufRead, Write}; +use std::io::Write; use std::path::Path; use std::process::exit; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{channel, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; -use std::{io, thread}; use verify::VerifyState; +mod consts; mod embedded; mod exercise; mod init; mod run; +mod tui; mod verify; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code @@ -37,7 +33,7 @@ enum Subcommands { Init, /// Verify all exercises according to the recommended order Verify, - /// Rerun `verify` when files were edited + /// Same as just running `rustlings` without a subcommand. Watch, /// Run/Test a single exercise Run { @@ -106,21 +102,20 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let command = args.command.unwrap_or_else(|| { - println!("{DEFAULT_OUT}\n"); - exit(0); - }); - - match command { + match args.command { + None | Some(Subcommands::Watch) => { + println!("{DEFAULT_OUT}\n"); + tui(&exercises)?; + } // `Init` is handled above. - Subcommands::Init => (), - Subcommands::List { + Some(Subcommands::Init) => (), + Some(Subcommands::List { paths, names, filter, unsolved, solved, - } => { + }) => { if !paths && !names { println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); } @@ -188,90 +183,30 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini ); exit(0); } - - Subcommands::Run { name } => { + Some(Subcommands::Run { name }) => { let exercise = find_exercise(&name, &exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); } - - Subcommands::Reset { name } => { + Some(Subcommands::Reset { name }) => { let exercise = find_exercise(&name, &exercises)?; EMBEDDED_FILES .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {exercise}"))?; println!("The file {} has been reset!", exercise.path.display()); } - - Subcommands::Hint { name } => { + Some(Subcommands::Hint { name }) => { let exercise = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } - - Subcommands::Verify => match verify(&exercises, (0, exercises.len()))? { + Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, - - Subcommands::Watch => match watch(&exercises) { - Err(e) => { - println!("Error: Could not watch your progress. Error message was {e:?}."); - println!("Most likely you've run out of disk space or your 'inotify limit' has been reached."); - exit(1); - } - Ok(WatchStatus::Finished) => { - println!( - "{emoji} All exercises completed! {emoji}", - emoji = Emoji("🎉", "★") - ); - println!("\n{FENISH_LINE}\n"); - } - Ok(WatchStatus::Unfinished) => { - println!("We hope you're enjoying learning about Rust!"); - println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again"); - } - }, } Ok(()) } -fn spawn_watch_shell( - failed_exercise_hint: Arc>>, - should_quit: Arc, -) { - println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here."); - - thread::spawn(move || { - let mut input = String::with_capacity(32); - let mut stdin = io::stdin().lock(); - - loop { - // Recycle input buffer. - input.clear(); - - if let Err(e) = stdin.read_line(&mut input) { - println!("error reading command: {e}"); - } - - let input = input.trim(); - if input == "hint" { - if let Some(hint) = &*failed_exercise_hint.lock().unwrap() { - println!("{hint}"); - } - } else if input == "clear" { - println!("\x1B[2J\x1B[1;1H"); - } else if input == "quit" { - should_quit.store(true, Ordering::SeqCst); - println!("Bye!"); - } else if input == "help" { - println!("{WATCH_MODE_HELP_MESSAGE}"); - } else { - println!("unknown command: {input}\n{WATCH_MODE_HELP_MESSAGE}"); - } - } - }); -} - fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> { if name == "next" { for exercise in exercises { @@ -290,147 +225,3 @@ fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exerci .find(|e| e.name == name) .with_context(|| format!("No exercise found for '{name}'!")) } - -enum WatchStatus { - Finished, - Unfinished, -} - -fn watch(exercises: &[Exercise]) -> Result { - /* Clears the terminal with an ANSI escape code. - Works in UNIX and newer Windows terminals. */ - fn clear_screen() { - println!("\x1Bc"); - } - - let (tx, rx) = channel(); - let should_quit = Arc::new(AtomicBool::new(false)); - - let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - - clear_screen(); - - let failed_exercise_hint = match verify(exercises, (0, exercises.len()))? { - VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), - VerifyState::Failed(exercise) => Arc::new(Mutex::new(Some(exercise.hint.clone()))), - }; - - spawn_watch_shell(Arc::clone(&failed_exercise_hint), Arc::clone(&should_quit)); - - let mut pending_exercises = Vec::with_capacity(exercises.len()); - loop { - match rx.recv_timeout(Duration::from_secs(1)) { - Ok(event) => match event { - Ok(events) => { - for event in events { - if event.kind == DebouncedEventKind::Any - && event.path.extension().is_some_and(|ext| ext == "rs") - { - pending_exercises.extend(exercises.iter().filter(|exercise| { - !exercise.looks_done().unwrap_or(false) - || event.path.ends_with(&exercise.path) - })); - let num_done = exercises.len() - pending_exercises.len(); - - clear_screen(); - - match verify( - pending_exercises.iter().copied(), - (num_done, exercises.len()), - )? { - VerifyState::AllExercisesDone => return Ok(WatchStatus::Finished), - VerifyState::Failed(exercise) => { - let hint = exercise.hint.clone(); - *failed_exercise_hint.lock().unwrap() = Some(hint); - } - } - - pending_exercises.clear(); - } - } - } - Err(e) => println!("watch error: {e:?}"), - }, - Err(RecvTimeoutError::Timeout) => { - // the timeout expired, just check the `should_quit` variable below then loop again - } - Err(e) => println!("watch error: {e:?}"), - } - // Check if we need to exit - if should_quit.load(Ordering::SeqCst) { - return Ok(WatchStatus::Unfinished); - } - } -} - -const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -const DEFAULT_OUT: &str = - "Is this your first time? Don't worry, Rustlings was made for beginners! We are -going to teach you a lot of things about Rust, but before we can get -started, here's a couple of notes about how Rustlings operates: - -1. The central concept behind Rustlings is that you solve exercises. These - exercises usually have some sort of syntax error in them, which will cause - them to fail compilation or testing. Sometimes there's a logic error instead - of a syntax error. No matter what error, it's your job to find it and fix it! - You'll know when you fixed it because then, the exercise will compile and - Rustlings will be able to move on to the next exercise. -2. If you run Rustlings in watch mode (which we recommend), it'll automatically - start with the first exercise. Don't get confused by an error message popping - up as soon as you run Rustlings! This is part of the exercise that you're - supposed to solve, so open the exercise file in an editor and start your - detective work! -3. If you're stuck on an exercise, there is a helpful hint you can view by typing - 'hint' (in watch mode), or running `rustlings hint exercise_name`. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, - and sometimes, other learners do too so you can help each other out! - -Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the `rustlings` directory!"; - -const WATCH_MODE_HELP_MESSAGE: &str = "Commands available to you in watch mode: - hint - prints the current exercise's hint - clear - clears the screen - quit - quits watch mode - help - displays this help message - -Watch mode automatically re-evaluates the current exercise -when you edit a file's contents."; - -const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/\x1b[31m - ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ - ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ - ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ - ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ - ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m - -We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, please don't hesitate to report them to our repo. -You can also contribute your own exercises to help the greater community! - -Before reporting an issue or contributing, please read our guidelines: -https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 00000000..bb873652 --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,92 @@ +use anyhow::Result; +use crossterm::{ + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebouncedEventKind}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::{ + io::stdout, + path::Path, + sync::mpsc::{channel, RecvTimeoutError}, + time::Duration, +}; + +use crate::{ + exercise::Exercise, + verify::{verify, VerifyState}, +}; + +fn watch(exercises: &[Exercise]) -> Result<()> { + let (tx, rx) = channel(); + + let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; + + let mut failed_exercise_hint = match verify(exercises, (0, exercises.len()))? { + VerifyState::AllExercisesDone => return Ok(()), + VerifyState::Failed(exercise) => Some(&exercise.hint), + }; + + let mut pending_exercises = Vec::with_capacity(exercises.len()); + loop { + match rx.recv_timeout(Duration::from_secs(1)) { + Ok(event) => match event { + Ok(events) => { + for event in events { + if event.kind == DebouncedEventKind::Any + && event.path.extension().is_some_and(|ext| ext == "rs") + { + pending_exercises.extend(exercises.iter().filter(|exercise| { + !exercise.looks_done().unwrap_or(false) + || event.path.ends_with(&exercise.path) + })); + let num_done = exercises.len() - pending_exercises.len(); + + match verify( + pending_exercises.iter().copied(), + (num_done, exercises.len()), + )? { + VerifyState::AllExercisesDone => return Ok(()), + VerifyState::Failed(exercise) => { + failed_exercise_hint = Some(&exercise.hint); + } + } + + pending_exercises.clear(); + } + } + } + Err(e) => println!("watch error: {e:?}"), + }, + Err(RecvTimeoutError::Timeout) => { + // the timeout expired, just check the `should_quit` variable below then loop again + } + Err(e) => println!("watch error: {e:?}"), + } + + // TODO: Check if we need to exit + } +} + +pub fn tui(exercises: &[Exercise]) -> Result<()> { + let mut stdout = stdout().lock(); + stdout.execute(EnterAlternateScreen)?; + enable_raw_mode()?; + let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; + terminal.clear()?; + + watch(exercises)?; + + drop(terminal); + stdout.execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + + // TODO + println!("We hope you're enjoying learning about Rust!"); + println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again"); + + Ok(()) +} diff --git a/src/verify.rs b/src/verify.rs index 5beb2069..aec2185c 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use console::style; +use crossterm::style::{Attribute, ContentStyle, Stylize}; use std::io::{stdout, Write}; use crate::exercise::{Exercise, Mode, State}; @@ -50,20 +50,26 @@ pub fn verify<'a>( println!( "\nYou can keep working on this exercise, or jump into the next one by removing the {} comment:\n", - style("`I AM NOT DONE`").bold() + "`I AM NOT DONE`".bold() ); for context_line in context { let formatted_line = if context_line.important { - format!("{}", style(context_line.line).bold()) + format!("{}", context_line.line.bold()) } else { context_line.line }; println!( "{:>2} {} {}", - style(context_line.number).blue().bold(), - style("|").blue(), + ContentStyle { + foreground_color: Some(crossterm::style::Color::Blue), + background_color: None, + underline_color: None, + attributes: Attribute::Bold.into() + } + .apply(context_line.number), + "|".blue(), formatted_line, ); } From 3f2d41de9ecd174ff2b099d3000bf7eca781779d Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 5 Apr 2024 03:05:07 +0200 Subject: [PATCH 044/433] Start with the state --- src/main.rs | 1 + src/state.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/state.rs diff --git a/src/main.rs b/src/main.rs index 47afd019..50517850 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod embedded; mod exercise; mod init; mod run; +mod state; mod tui; mod verify; diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 00000000..e3e32990 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,32 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::{fs, io, path::PathBuf}; + +#[derive(Serialize, Deserialize)] +pub struct ExerciseState { + pub path: PathBuf, + pub done: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct State { + pub progress: Vec, +} + +impl State { + pub fn read() -> Result { + let file_content = + fs::read(".rustlings.json").context("Failed to read the file `.rustlings.json`")?; + + serde_json::de::from_slice(&file_content) + .context("Failed to deserialize the file `.rustlings.json`") + } + + pub fn write(&self) -> io::Result<()> { + // TODO: Capacity + let mut buf = Vec::with_capacity(1 << 12); + serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state"); + dbg!(buf.len()); + Ok(()) + } +} From 60155294e94acd661e4fe20cf8b72412167c772d Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 6 Apr 2024 01:45:54 +0200 Subject: [PATCH 045/433] Rename packages --- dev/Cargo.toml | 2 +- gen-dev-cargo-toml/src/main.rs | 2 +- tests/fixture/failure/Cargo.toml | 2 +- tests/fixture/state/Cargo.toml | 2 +- tests/fixture/success/Cargo.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index ed9b3ed3..1d230ebb 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -101,6 +101,6 @@ bin = [ ] [package] -name = "rustlings" +name = "rustlings-dev" edition = "2021" publish = false diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs index 622762ad..9a7c1bbd 100644 --- a/gen-dev-cargo-toml/src/main.rs +++ b/gen-dev-cargo-toml/src/main.rs @@ -48,7 +48,7 @@ bin = [\n", br#"] [package] -name = "rustlings" +name = "rustlings-dev" edition = "2021" publish = false "#, diff --git a/tests/fixture/failure/Cargo.toml b/tests/fixture/failure/Cargo.toml index e111cf2b..7ee2f068 100644 --- a/tests/fixture/failure/Cargo.toml +++ b/tests/fixture/failure/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tests" +name = "failure" edition = "2021" publish = false diff --git a/tests/fixture/state/Cargo.toml b/tests/fixture/state/Cargo.toml index c8d74e47..adbd8ab1 100644 --- a/tests/fixture/state/Cargo.toml +++ b/tests/fixture/state/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tests" +name = "state" edition = "2021" publish = false diff --git a/tests/fixture/success/Cargo.toml b/tests/fixture/success/Cargo.toml index f26a44f1..028cf35a 100644 --- a/tests/fixture/success/Cargo.toml +++ b/tests/fixture/success/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "tests" +name = "success" edition = "2021" publish = false From 06e7216c833f46299c0314bbab47f8df9fc355a3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 6 Apr 2024 01:46:09 +0200 Subject: [PATCH 046/433] Elimintate an itermediate variable --- tests/integration_tests.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d853521f..ccdd910e 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -7,8 +7,7 @@ use std::process::Command; #[test] fn runs_without_arguments() { - let mut cmd = Command::cargo_bin("rustlings").unwrap(); - cmd.assert().success(); + Command::cargo_bin("rustlings").unwrap().assert().success(); } #[test] From de9a0ed5221934b43a27921455f484e006c3ec20 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 6 Apr 2024 01:46:22 +0200 Subject: [PATCH 047/433] Update state --- src/state.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/state.rs b/src/state.rs index e3e32990..60f6a379 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,31 +1,37 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use std::{fs, io, path::PathBuf}; +use std::fs; -#[derive(Serialize, Deserialize)] -pub struct ExerciseState { - pub path: PathBuf, - pub done: bool, -} +use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] pub struct State { - pub progress: Vec, + pub progress: Vec, } impl State { - pub fn read() -> Result { - let file_content = - fs::read(".rustlings.json").context("Failed to read the file `.rustlings.json`")?; + fn read(exercises: &[Exercise]) -> Option { + let file_content = fs::read(".rustlings.json").ok()?; - serde_json::de::from_slice(&file_content) - .context("Failed to deserialize the file `.rustlings.json`") + let slf: Self = serde_json::de::from_slice(&file_content).ok()?; + + if slf.progress.len() != exercises.len() { + return None; + } + + Some(slf) } - pub fn write(&self) -> io::Result<()> { + pub fn read_or_default(exercises: &[Exercise]) -> Self { + Self::read(exercises).unwrap_or_else(|| Self { + progress: vec![false; exercises.len()], + }) + } + + pub fn write(&self) -> Result<()> { // TODO: Capacity let mut buf = Vec::with_capacity(1 << 12); - serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state"); + serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; dbg!(buf.len()); Ok(()) } From c2daad8340c04eaa84525f6ee832972667068fd6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 01:15:47 +0200 Subject: [PATCH 048/433] Return an error instead of exiting --- src/exercise.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index d5ca254e..d01d427a 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Debug, Display, Formatter}; use std::fs::{self, File}; use std::io::{self, BufRead, BufReader}; use std::path::PathBuf; -use std::process::{exit, Command, Output}; +use std::process::{Command, Output}; use std::{array, mem}; use winnow::ascii::{space0, Caseless}; use winnow::combinator::opt; @@ -145,13 +145,9 @@ impl Exercise { let mut line = String::with_capacity(256); loop { - let n = read_line(&mut line).unwrap_or_else(|e| { - println!( - "Failed to read the exercise file {}: {e}", - self.path.display(), - ); - exit(1); - }); + let n = read_line(&mut line).with_context(|| { + format!("Failed to read the exercise file {}", self.path.display()) + })?; // Reached the end of the file and didn't find the comment. if n == 0 { From 18342b3aa3bd43c2c013614935f45e7d6bbaea8f Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 01:16:56 +0200 Subject: [PATCH 049/433] Verify starting with some index --- src/verify.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/verify.rs b/src/verify.rs index aec2185c..c4368cc7 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -14,17 +14,16 @@ pub enum VerifyState<'a> { // Any such failures will be reported to the end user. // If the Exercise being verified is a test, the verbose boolean // determines whether or not the test harness outputs are displayed. -pub fn verify<'a>( - pending_exercises: impl IntoIterator, - progress: (usize, usize), -) -> Result> { - let (mut num_done, total) = progress; - println!( - "Progress: {num_done}/{total} ({:.1}%)\n", - num_done as f32 / total as f32 * 100.0, - ); +pub fn verify(exercises: &[Exercise], mut current_exercise_ind: usize) -> Result> { + while current_exercise_ind < exercises.len() { + let exercise = &exercises[current_exercise_ind]; + + println!( + "Progress: {current_exercise_ind}/{} ({:.1}%)\n", + exercises.len(), + current_exercise_ind as f32 / exercises.len() as f32 * 100.0, + ); - for exercise in pending_exercises { let output = exercise.run()?; { @@ -76,11 +75,7 @@ or jump into the next one by removing the {} comment:\n", return Ok(VerifyState::Failed(exercise)); } - num_done += 1; - println!( - "Progress: {num_done}/{total} ({:.1}%)\n", - num_done as f32 / total as f32 * 100.0, - ); + current_exercise_ind += 1; } Ok(VerifyState::AllExercisesDone) From 0819bbe21fc86315d3acdcdb2bc14b21f3acb788 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 01:17:53 +0200 Subject: [PATCH 050/433] Can't use Ratatui for the watch mode :( --- src/main.rs | 22 ++--- src/tui.rs | 92 -------------------- src/watch.rs | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 103 deletions(-) delete mode 100644 src/tui.rs create mode 100644 src/watch.rs diff --git a/src/main.rs b/src/main.rs index 50517850..e8218efe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ -use crate::consts::{DEFAULT_OUT, WELCOME}; +use crate::consts::WELCOME; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; use crate::run::run; -use crate::tui::tui; use crate::verify::verify; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; +use state::State; use std::io::Write; use std::path::Path; use std::process::exit; @@ -17,8 +17,8 @@ mod exercise; mod init; mod run; mod state; -mod tui; mod verify; +mod watch; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] @@ -75,10 +75,6 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - if args.command.is_none() { - println!("\n{WELCOME}\n"); - } - which::which("cargo").context( "Failed to find `cargo`. Did you already install Rust? @@ -97,16 +93,20 @@ Then run `rustlings` for further instructions on getting started." return Ok(()); } else if !Path::new("exercises").is_dir() { println!( - "\nThe `exercises` directory wasn't found in the current directory. + " +{WELCOME} + +The `exercises` directory wasn't found in the current directory. If you are just starting with Rustlings, run the command `rustlings init` to initialize it." ); exit(1); } + let state = State::read_or_default(&exercises); + match args.command { None | Some(Subcommands::Watch) => { - println!("{DEFAULT_OUT}\n"); - tui(&exercises)?; + watch::watch(&state, &exercises)?; } // `Init` is handled above. Some(Subcommands::Init) => (), @@ -199,7 +199,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let exercise = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } - Some(Subcommands::Verify) => match verify(&exercises, (0, exercises.len()))? { + Some(Subcommands::Verify) => match verify(&exercises, 0)? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, diff --git a/src/tui.rs b/src/tui.rs deleted file mode 100644 index bb873652..00000000 --- a/src/tui.rs +++ /dev/null @@ -1,92 +0,0 @@ -use anyhow::Result; -use crossterm::{ - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - ExecutableCommand, -}; -use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebouncedEventKind}; -use ratatui::{backend::CrosstermBackend, Terminal}; -use std::{ - io::stdout, - path::Path, - sync::mpsc::{channel, RecvTimeoutError}, - time::Duration, -}; - -use crate::{ - exercise::Exercise, - verify::{verify, VerifyState}, -}; - -fn watch(exercises: &[Exercise]) -> Result<()> { - let (tx, rx) = channel(); - - let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - - let mut failed_exercise_hint = match verify(exercises, (0, exercises.len()))? { - VerifyState::AllExercisesDone => return Ok(()), - VerifyState::Failed(exercise) => Some(&exercise.hint), - }; - - let mut pending_exercises = Vec::with_capacity(exercises.len()); - loop { - match rx.recv_timeout(Duration::from_secs(1)) { - Ok(event) => match event { - Ok(events) => { - for event in events { - if event.kind == DebouncedEventKind::Any - && event.path.extension().is_some_and(|ext| ext == "rs") - { - pending_exercises.extend(exercises.iter().filter(|exercise| { - !exercise.looks_done().unwrap_or(false) - || event.path.ends_with(&exercise.path) - })); - let num_done = exercises.len() - pending_exercises.len(); - - match verify( - pending_exercises.iter().copied(), - (num_done, exercises.len()), - )? { - VerifyState::AllExercisesDone => return Ok(()), - VerifyState::Failed(exercise) => { - failed_exercise_hint = Some(&exercise.hint); - } - } - - pending_exercises.clear(); - } - } - } - Err(e) => println!("watch error: {e:?}"), - }, - Err(RecvTimeoutError::Timeout) => { - // the timeout expired, just check the `should_quit` variable below then loop again - } - Err(e) => println!("watch error: {e:?}"), - } - - // TODO: Check if we need to exit - } -} - -pub fn tui(exercises: &[Exercise]) -> Result<()> { - let mut stdout = stdout().lock(); - stdout.execute(EnterAlternateScreen)?; - enable_raw_mode()?; - let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; - terminal.clear()?; - - watch(exercises)?; - - drop(terminal); - stdout.execute(LeaveAlternateScreen)?; - disable_raw_mode()?; - - // TODO - println!("We hope you're enjoying learning about Rust!"); - println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again"); - - Ok(()) -} diff --git a/src/watch.rs b/src/watch.rs new file mode 100644 index 00000000..92da20dd --- /dev/null +++ b/src/watch.rs @@ -0,0 +1,240 @@ +use anyhow::Result; +use crossterm::{ + style::{Attribute, ContentStyle, Stylize}, + terminal::{Clear, ClearType}, + ExecutableCommand, +}; +use notify_debouncer_mini::{ + new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, +}; +use std::{ + fmt::Write as _, + io::{self, BufRead, StdoutLock, Write}, + path::Path, + sync::mpsc::{channel, sync_channel, Receiver}, + thread, + time::Duration, +}; + +use crate::{ + exercise::{self, Exercise}, + state::State, +}; + +enum Event { + Hint, + Clear, + Quit, +} + +struct WatchState<'a> { + writer: StdoutLock<'a>, + rx: Receiver, + exercises: &'a [Exercise], + exercise: &'a Exercise, + current_exercise_ind: usize, + stdout: Option>, + stderr: Option>, + message: Option, + prompt: Vec, +} + +impl<'a> WatchState<'a> { + fn run_exercise(&mut self) -> Result { + let output = self.exercise.run()?; + + if !output.status.success() { + self.stdout = Some(output.stdout); + self.stderr = Some(output.stderr); + return Ok(false); + } + + if let exercise::State::Pending(context) = self.exercise.state()? { + let mut message = format!( + " +You can keep working on this exercise or jump into the next one by removing the {} comment: + +", + "`I AM NOT DONE`".bold(), + ); + + for context_line in context { + let formatted_line = if context_line.important { + context_line.line.bold() + } else { + context_line.line.stylize() + }; + + writeln!( + message, + "{:>2} {} {}", + ContentStyle { + foreground_color: Some(crossterm::style::Color::Blue), + background_color: None, + underline_color: None, + attributes: Attribute::Bold.into() + } + .apply(context_line.number), + "|".blue(), + formatted_line, + )?; + } + + self.stdout = Some(output.stdout); + self.message = Some(message); + return Ok(false); + } + + Ok(true) + } + + fn try_recv_event(&mut self) -> Result<()> { + let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { + return Ok(()); + }; + + if let Some(current_exercise_ind) = events? + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + { + self.current_exercise_ind = current_exercise_ind; + } else { + return Ok(()); + }; + + while self.current_exercise_ind < self.exercises.len() { + self.exercise = &self.exercises[self.current_exercise_ind]; + if !self.run_exercise()? { + break; + } + + self.current_exercise_ind += 1; + } + + Ok(()) + } + + fn prompt(&mut self) -> io::Result<()> { + self.writer.write_all(&self.prompt)?; + self.writer.flush() + } + + fn render(&mut self) -> Result<()> { + self.writer.execute(Clear(ClearType::All))?; + + if let Some(stdout) = &self.stdout { + self.writer.write_all(stdout)?; + } + + if let Some(stderr) = &self.stderr { + self.writer.write_all(stderr)?; + } + + if let Some(message) = &self.message { + self.writer.write_all(message.as_bytes())?; + } + + self.prompt()?; + + Ok(()) + } +} + +pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> { + let (tx, rx) = channel(); + let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive)?; + + let current_exercise_ind = state.progress.iter().position(|done| *done).unwrap_or(0); + + let exercise = &exercises[current_exercise_ind]; + + let writer = io::stdout().lock(); + + let mut watch_state = WatchState { + writer, + rx, + exercises, + exercise, + current_exercise_ind, + stdout: None, + stderr: None, + message: None, + prompt: format!( + "\n\n{}int/{}lear/{}uit? ", + "h".bold(), + "c".bold(), + "q".bold() + ) + .into_bytes(), + }; + + watch_state.run_exercise()?; + watch_state.render()?; + + let (tx, rx) = sync_channel(0); + thread::spawn(move || { + let mut stdin = io::stdin().lock(); + let mut stdin_buf = String::with_capacity(8); + + loop { + stdin.read_line(&mut stdin_buf).unwrap(); + + let event = match stdin_buf.trim() { + "h" | "hint" => Some(Event::Hint), + "c" | "clear" => Some(Event::Clear), + "q" | "quit" => Some(Event::Quit), + _ => None, + }; + + stdin_buf.clear(); + + if tx.send(event).is_err() { + break; + }; + } + }); + + loop { + watch_state.try_recv_event()?; + + if let Ok(event) = rx.try_recv() { + match event { + Some(Event::Hint) => { + watch_state + .writer + .write_all(watch_state.exercise.hint.as_bytes())?; + watch_state.prompt()?; + } + Some(Event::Clear) => { + watch_state.render()?; + } + Some(Event::Quit) => break, + None => { + watch_state.writer.write_all(b"Invalid command")?; + watch_state.prompt()?; + } + } + } + } + + watch_state.writer.write_all(b" +We hope you're enjoying learning Rust! +If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. +")?; + + Ok(()) +} From f6db88aca860b229e97712a612cee8ab4436b764 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 03:03:37 +0200 Subject: [PATCH 051/433] Started with list --- src/list.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 96 +++-------------------------------------------------- 2 files changed, 97 insertions(+), 92 deletions(-) create mode 100644 src/list.rs diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 00000000..f8713b07 --- /dev/null +++ b/src/list.rs @@ -0,0 +1,93 @@ +use std::{io, time::Duration}; + +use anyhow::Result; +use crossterm::{ + event::{self, KeyCode, KeyEventKind}, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, + ExecutableCommand, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::Constraint, + style::{Modifier, Style, Stylize}, + text::Span, + widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, + Terminal, +}; + +use crate::{exercise::Exercise, state::State}; + +// 40 FPS. +const UPDATE_INTERVAL: Duration = Duration::from_millis(25); + +pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { + let mut stdout = io::stdout().lock(); + + stdout.execute(EnterAlternateScreen)?; + enable_raw_mode()?; + + let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; + terminal.clear()?; + + let header = Row::new(["State", "Name", "Path"]); + + let max_name_len = exercises + .iter() + .map(|exercise| exercise.name.len()) + .max() + .unwrap_or(4) as u16; + + let widths = [ + Constraint::Length(7), + Constraint::Length(max_name_len), + Constraint::Fill(1), + ]; + + let rows = exercises + .iter() + .zip(&state.progress) + .map(|(exercise, done)| { + let state = if *done { + "DONE".green() + } else { + "PENDING".yellow() + }; + Row::new([ + state, + Span::raw(&exercise.name), + Span::raw(exercise.path.to_string_lossy()), + ]) + }) + .collect::>(); + + let table = Table::new(rows, widths) + .header(header) + .column_spacing(2) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) + .highlight_symbol("🦀"); + + let mut table_state = TableState::default().with_selected(Some(0)); + + loop { + terminal.draw(|frame| { + let area = frame.size(); + + frame.render_stateful_widget(&table, area, &mut table_state); + })?; + + if event::poll(UPDATE_INTERVAL)? { + if let event::Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + break; + } + } + } + } + + drop(terminal); + stdout.execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e8218efe..34d1784a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use crate::verify::verify; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use state::State; -use std::io::Write; use std::path::Path; use std::process::exit; use verify::VerifyState; @@ -15,6 +14,7 @@ mod consts; mod embedded; mod exercise; mod init; +mod list; mod run; mod state; mod verify; @@ -52,24 +52,7 @@ enum Subcommands { name: String, }, /// List the exercises available in Rustlings - List { - /// Show only the paths of the exercises - #[arg(short, long)] - paths: bool, - /// Show only the names of the exercises - #[arg(short, long)] - names: bool, - /// Provide a string to match exercise names. - /// Comma separated patterns are accepted - #[arg(short, long)] - filter: Option, - /// Display only exercises not yet solved - #[arg(short, long)] - unsolved: bool, - /// Display only exercises that have been solved - #[arg(short, long)] - solved: bool, - }, + List, } fn main() -> Result<()> { @@ -110,79 +93,8 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini } // `Init` is handled above. Some(Subcommands::Init) => (), - Some(Subcommands::List { - paths, - names, - filter, - unsolved, - solved, - }) => { - if !paths && !names { - println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status"); - } - let mut exercises_done: u16 = 0; - let lowercase_filter = filter - .as_ref() - .map(|s| s.to_lowercase()) - .unwrap_or_default(); - let filters = lowercase_filter - .split(',') - .filter_map(|f| { - let f = f.trim(); - if f.is_empty() { - None - } else { - Some(f) - } - }) - .collect::>(); - - for exercise in &exercises { - let fname = exercise.path.to_string_lossy(); - let filter_cond = filters - .iter() - .any(|f| exercise.name.contains(f) || fname.contains(f)); - let looks_done = exercise.looks_done()?; - let status = if looks_done { - exercises_done += 1; - "Done" - } else { - "Pending" - }; - let solve_cond = - (looks_done && solved) || (!looks_done && unsolved) || (!solved && !unsolved); - if solve_cond && (filter_cond || filter.is_none()) { - let line = if paths { - format!("{fname}\n") - } else if names { - format!("{}\n", exercise.name) - } else { - format!("{:<17}\t{fname:<46}\t{status:<7}\n", exercise.name) - }; - // Somehow using println! leads to the binary panicking - // when its output is piped. - // So, we're handling a Broken Pipe error and exiting with 0 anyway - let stdout = std::io::stdout(); - { - let mut handle = stdout.lock(); - handle.write_all(line.as_bytes()).unwrap_or_else(|e| { - match e.kind() { - std::io::ErrorKind::BrokenPipe => exit(0), - _ => exit(1), - }; - }); - } - } - } - - let percentage_progress = exercises_done as f32 / exercises.len() as f32 * 100.0; - println!( - "Progress: You completed {} / {} exercises ({:.1} %).", - exercises_done, - exercises.len(), - percentage_progress - ); - exit(0); + Some(Subcommands::List) => { + list::list(&state, &exercises)?; } Some(Subcommands::Run { name }) => { let exercise = find_exercise(&name, &exercises)?; From 729385362c06da0c90015bb2d4b6b341d2cd489b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 03:03:59 +0200 Subject: [PATCH 052/433] Update deps --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33d3030a..ee469437 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,9 +711,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" From 372290a796eb27b28edaf2475ebbb4e6e09090b3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 03:38:18 +0200 Subject: [PATCH 053/433] Done navigation --- src/list.rs | 83 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/src/list.rs b/src/list.rs index f8713b07..82c3e465 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,34 +1,22 @@ -use std::{io, time::Duration}; - use anyhow::Result; use crossterm::{ - event::{self, KeyCode, KeyEventKind}, + event::{self, Event, KeyCode, KeyEventKind}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; use ratatui::{ backend::CrosstermBackend, layout::Constraint, - style::{Modifier, Style, Stylize}, + style::{Style, Stylize}, text::Span, - widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, + widgets::{HighlightSpacing, Row, Table, TableState}, Terminal, }; +use std::io; use crate::{exercise::Exercise, state::State}; -// 40 FPS. -const UPDATE_INTERVAL: Duration = Duration::from_millis(25); - -pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { - let mut stdout = io::stdout().lock(); - - stdout.execute(EnterAlternateScreen)?; - enable_raw_mode()?; - - let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; - terminal.clear()?; - +fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { let header = Row::new(["State", "Name", "Path"]); let max_name_len = exercises @@ -60,28 +48,69 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { }) .collect::>(); - let table = Table::new(rows, widths) + Table::new(rows, widths) .header(header) .column_spacing(2) .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) - .highlight_symbol("🦀"); + .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) + .highlight_symbol("🦀") +} - let mut table_state = TableState::default().with_selected(Some(0)); +pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { + let mut stdout = io::stdout().lock(); - loop { + stdout.execute(EnterAlternateScreen)?; + enable_raw_mode()?; + + let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; + terminal.clear()?; + + let table = table(state, exercises); + + let last_ind = exercises.len() - 1; + let mut selected = 0; + let mut table_state = TableState::default().with_selected(Some(selected)); + + 'outer: loop { terminal.draw(|frame| { let area = frame.size(); frame.render_stateful_widget(&table, area, &mut table_state); })?; - if event::poll(UPDATE_INTERVAL)? { - if let event::Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { - break; - } + let key = loop { + match event::read()? { + Event::Key(key) => break key, + // Redraw + Event::Resize(_, _) => continue 'outer, + // Ignore + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => (), } + }; + + if key.kind != KeyEventKind::Press { + continue; + } + + match key.code { + KeyCode::Char('q') => break, + KeyCode::Down | KeyCode::Char('j') => { + selected = selected.saturating_add(1).min(last_ind); + table_state.select(Some(selected)); + } + KeyCode::Up | KeyCode::Char('k') => { + selected = selected.saturating_sub(1).max(0); + table_state.select(Some(selected)); + } + KeyCode::Home | KeyCode::Char('g') => { + selected = 0; + table_state.select(Some(selected)); + } + KeyCode::End | KeyCode::Char('G') => { + selected = last_ind; + table_state.select(Some(selected)); + } + _ => (), } } From c4897139aeff2316d2b737a4e03b7491b696ce3b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 03:41:23 +0200 Subject: [PATCH 054/433] Prevent unneeded redraws --- src/list.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/list.rs b/src/list.rs index 82c3e465..b8ea27bb 100644 --- a/src/list.rs +++ b/src/list.rs @@ -80,7 +80,13 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { let key = loop { match event::read()? { - Event::Key(key) => break key, + Event::Key(key) => { + if key.kind != KeyEventKind::Press { + continue; + } + + break key; + } // Redraw Event::Resize(_, _) => continue 'outer, // Ignore @@ -88,10 +94,6 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { } }; - if key.kind != KeyEventKind::Press { - continue; - } - match key.code { KeyCode::Char('q') => break, KeyCode::Down | KeyCode::Char('j') => { From 7f5a18fa3478596c3c1dbdc7eb92da99b0945886 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 04:19:50 +0200 Subject: [PATCH 055/433] Show help message --- src/list.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/list.rs b/src/list.rs index b8ea27bb..7329d2b8 100644 --- a/src/list.rs +++ b/src/list.rs @@ -6,10 +6,10 @@ use crossterm::{ }; use ratatui::{ backend::CrosstermBackend, - layout::Constraint, + layout::{Constraint, Rect}, style::{Style, Stylize}, - text::Span, - widgets::{HighlightSpacing, Row, Table, TableState}, + text::{Line, Span}, + widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, Terminal, }; use std::io; @@ -54,6 +54,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { .highlight_spacing(HighlightSpacing::Always) .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) .highlight_symbol("🦀") + .block(Block::default().borders(Borders::BOTTOM)) } pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { @@ -75,7 +76,25 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { terminal.draw(|frame| { let area = frame.size(); - frame.render_stateful_widget(&table, area, &mut table_state); + frame.render_stateful_widget( + &table, + Rect { + x: 0, + y: 0, + width: area.width, + height: area.height - 1, + }, + &mut table_state, + ); + frame.render_widget( + Span::raw("Navi: ↓/j ↑/k home/g end/G │ Filter done/pending: d/p │ Reset: r │ Continue at: c │ Quit: q"), + Rect { + x: 0, + y: area.height - 1, + width: area.width, + height: 1, + }, + ); })?; let key = loop { From e640b4a1ffec82cba6b34c0bd222f4ab65502daa Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 04:36:27 +0200 Subject: [PATCH 056/433] Add "Next" column --- src/list.rs | 20 +++++++++++++++----- src/state.rs | 4 +++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/list.rs b/src/list.rs index 7329d2b8..ce809efe 100644 --- a/src/list.rs +++ b/src/list.rs @@ -8,7 +8,7 @@ use ratatui::{ backend::CrosstermBackend, layout::{Constraint, Rect}, style::{Style, Stylize}, - text::{Line, Span}, + text::Span, widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, Terminal, }; @@ -17,7 +17,7 @@ use std::io; use crate::{exercise::Exercise, state::State}; fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { - let header = Row::new(["State", "Name", "Path"]); + let header = Row::new(["Next", "State", "Name", "Path"]); let max_name_len = exercises .iter() @@ -26,6 +26,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { .unwrap_or(4) as u16; let widths = [ + Constraint::Length(4), Constraint::Length(7), Constraint::Length(max_name_len), Constraint::Fill(1), @@ -34,14 +35,23 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { let rows = exercises .iter() .zip(&state.progress) - .map(|(exercise, done)| { - let state = if *done { + .enumerate() + .map(|(ind, (exercise, done))| { + let exercise_state = if *done { "DONE".green() } else { "PENDING".yellow() }; + + let next = if ind == state.next_exercise_ind { + ">>>>".bold().red() + } else { + Span::default() + }; + Row::new([ - state, + next, + exercise_state, Span::raw(&exercise.name), Span::raw(exercise.path.to_string_lossy()), ]) diff --git a/src/state.rs b/src/state.rs index 60f6a379..f29dc135 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,6 +6,7 @@ use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] pub struct State { + pub next_exercise_ind: usize, pub progress: Vec, } @@ -15,7 +16,7 @@ impl State { let slf: Self = serde_json::de::from_slice(&file_content).ok()?; - if slf.progress.len() != exercises.len() { + if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() { return None; } @@ -24,6 +25,7 @@ impl State { pub fn read_or_default(exercises: &[Exercise]) -> Self { Self::read(exercises).unwrap_or_else(|| Self { + next_exercise_ind: 0, progress: vec![false; exercises.len()], }) } From 4f69285375342951da36346f1a1b93f7903a362f Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 04:39:03 +0200 Subject: [PATCH 057/433] Shorten the help footer --- src/list.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index ce809efe..ff031cb0 100644 --- a/src/list.rs +++ b/src/list.rs @@ -96,8 +96,10 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { }, &mut table_state, ); + + // Help footer frame.render_widget( - Span::raw("Navi: ↓/j ↑/k home/g end/G │ Filter done/pending: d/p │ Reset: r │ Continue at: c │ Quit: q"), + Span::raw("↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"), Rect { x: 0, y: area.height - 1, From b0a475062445705853b4f861ee9e3135065f0660 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 04:59:22 +0200 Subject: [PATCH 058/433] Implement "continue at" --- src/list.rs | 66 +++++++++++++++++++++++++++++----------------------- src/main.rs | 4 ++-- src/state.rs | 31 +++++++++++++++++++----- src/watch.rs | 2 +- 4 files changed, 65 insertions(+), 38 deletions(-) diff --git a/src/list.rs b/src/list.rs index ff031cb0..bb5ba1c0 100644 --- a/src/list.rs +++ b/src/list.rs @@ -16,6 +16,36 @@ use std::io; use crate::{exercise::Exercise, state::State}; +fn rows<'s, 'e>(state: &'s State, exercises: &'e [Exercise]) -> impl Iterator> + 's +where + 'e: 's, +{ + exercises + .iter() + .zip(state.progress()) + .enumerate() + .map(|(ind, (exercise, done))| { + let exercise_state = if *done { + "DONE".green() + } else { + "PENDING".yellow() + }; + + let next = if ind == state.next_exercise_ind() { + ">>>>".bold().red() + } else { + Span::default() + }; + + Row::new([ + next, + exercise_state, + Span::raw(&exercise.name), + Span::raw(exercise.path.to_string_lossy()), + ]) + }) +} + fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { let header = Row::new(["Next", "State", "Name", "Path"]); @@ -32,33 +62,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { Constraint::Fill(1), ]; - let rows = exercises - .iter() - .zip(&state.progress) - .enumerate() - .map(|(ind, (exercise, done))| { - let exercise_state = if *done { - "DONE".green() - } else { - "PENDING".yellow() - }; - - let next = if ind == state.next_exercise_ind { - ">>>>".bold().red() - } else { - Span::default() - }; - - Row::new([ - next, - exercise_state, - Span::raw(&exercise.name), - Span::raw(exercise.path.to_string_lossy()), - ]) - }) - .collect::>(); - - Table::new(rows, widths) + Table::new(rows(state, exercises), widths) .header(header) .column_spacing(2) .highlight_spacing(HighlightSpacing::Always) @@ -67,7 +71,7 @@ fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { .block(Block::default().borders(Borders::BOTTOM)) } -pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { +pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; @@ -76,7 +80,7 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let table = table(state, exercises); + let mut table = table(state, exercises); let last_ind = exercises.len() - 1; let mut selected = 0; @@ -143,6 +147,10 @@ pub fn list(state: &State, exercises: &[Exercise]) -> Result<()> { selected = last_ind; table_state.select(Some(selected)); } + KeyCode::Char('c') => { + state.set_next_exercise_ind(selected)?; + table = table.rows(rows(state, exercises)); + } _ => (), } } diff --git a/src/main.rs b/src/main.rs index 34d1784a..e82fc808 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let state = State::read_or_default(&exercises); + let mut state = State::read_or_default(&exercises); match args.command { None | Some(Subcommands::Watch) => { @@ -94,7 +94,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::List) => { - list::list(&state, &exercises)?; + list::list(&mut state, &exercises)?; } Some(Subcommands::Run { name }) => { let exercise = find_exercise(&name, &exercises)?; diff --git a/src/state.rs b/src/state.rs index f29dc135..5a644873 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use std::fs; @@ -6,8 +6,8 @@ use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] pub struct State { - pub next_exercise_ind: usize, - pub progress: Vec, + next_exercise_ind: usize, + progress: Vec, } impl State { @@ -30,11 +30,30 @@ impl State { }) } - pub fn write(&self) -> Result<()> { + fn write(&self) -> Result<()> { // TODO: Capacity - let mut buf = Vec::with_capacity(1 << 12); + let mut buf = Vec::with_capacity(1024); serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - dbg!(buf.len()); + Ok(()) } + + #[inline] + pub fn next_exercise_ind(&self) -> usize { + self.next_exercise_ind + } + + pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { + if ind >= self.progress.len() { + bail!("The next exercise index is higher than the number of exercises"); + } + + self.next_exercise_ind = ind; + self.write() + } + + #[inline] + pub fn progress(&self) -> &[bool] { + &self.progress + } } diff --git a/src/watch.rs b/src/watch.rs index 92da20dd..cc9668d0 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -158,7 +158,7 @@ pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> { .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state.progress.iter().position(|done| *done).unwrap_or(0); + let current_exercise_ind = state.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; From 2db86833a9f3fae4dc5410aac828b3071dda1984 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 13:12:40 +0200 Subject: [PATCH 059/433] Fix lifetimes --- src/list.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/list.rs b/src/list.rs index bb5ba1c0..5153e01f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -16,27 +16,31 @@ use std::io; use crate::{exercise::Exercise, state::State}; -fn rows<'s, 'e>(state: &'s State, exercises: &'e [Exercise]) -> impl Iterator> + 's +fn rows<'s, 'e, 'i>( + state: &'s State, + exercises: &'e [Exercise], +) -> impl Iterator> + 'i where - 'e: 's, + 's: 'i, + 'e: 'i, { exercises .iter() .zip(state.progress()) .enumerate() .map(|(ind, (exercise, done))| { - let exercise_state = if *done { - "DONE".green() - } else { - "PENDING".yellow() - }; - let next = if ind == state.next_exercise_ind() { ">>>>".bold().red() } else { Span::default() }; + let exercise_state = if *done { + "DONE".green() + } else { + "PENDING".yellow() + }; + Row::new([ next, exercise_state, From d988054ad851cb6ce67c77e2607322142d188804 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 16:33:00 +0200 Subject: [PATCH 060/433] Add UiState --- src/list.rs | 236 +++++++++++++++++++++++++++++----------------------- 1 file changed, 134 insertions(+), 102 deletions(-) diff --git a/src/list.rs b/src/list.rs index 5153e01f..dad21822 100644 --- a/src/list.rs +++ b/src/list.rs @@ -10,112 +10,156 @@ use ratatui::{ style::{Style, Stylize}, text::Span, widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, - Terminal, + Frame, Terminal, }; use std::io; use crate::{exercise::Exercise, state::State}; -fn rows<'s, 'e, 'i>( - state: &'s State, - exercises: &'e [Exercise], -) -> impl Iterator> + 'i -where - 's: 'i, - 'e: 'i, -{ - exercises - .iter() - .zip(state.progress()) - .enumerate() - .map(|(ind, (exercise, done))| { - let next = if ind == state.next_exercise_ind() { - ">>>>".bold().red() - } else { - Span::default() - }; - - let exercise_state = if *done { - "DONE".green() - } else { - "PENDING".yellow() - }; - - Row::new([ - next, - exercise_state, - Span::raw(&exercise.name), - Span::raw(exercise.path.to_string_lossy()), - ]) - }) +struct UiState<'a> { + pub table: Table<'a>, + selected: usize, + table_state: TableState, + last_ind: usize, } -fn table<'a>(state: &State, exercises: &'a [Exercise]) -> Table<'a> { - let header = Row::new(["Next", "State", "Name", "Path"]); +impl<'a> UiState<'a> { + pub fn rows<'s, 'i>( + state: &'s State, + exercises: &'a [Exercise], + ) -> impl Iterator> + 'i + where + 's: 'i, + 'a: 'i, + { + exercises + .iter() + .zip(state.progress()) + .enumerate() + .map(|(ind, (exercise, done))| { + let next = if ind == state.next_exercise_ind() { + ">>>>".bold().red() + } else { + Span::default() + }; - let max_name_len = exercises - .iter() - .map(|exercise| exercise.name.len()) - .max() - .unwrap_or(4) as u16; + let exercise_state = if *done { + "DONE".green() + } else { + "PENDING".yellow() + }; - let widths = [ - Constraint::Length(4), - Constraint::Length(7), - Constraint::Length(max_name_len), - Constraint::Fill(1), - ]; + Row::new([ + next, + exercise_state, + Span::raw(&exercise.name), + Span::raw(exercise.path.to_string_lossy()), + ]) + }) + } - Table::new(rows(state, exercises), widths) - .header(header) - .column_spacing(2) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) - .highlight_symbol("🦀") - .block(Block::default().borders(Borders::BOTTOM)) + pub fn new(state: &State, exercises: &'a [Exercise]) -> Self { + let header = Row::new(["Next", "State", "Name", "Path"]); + + let max_name_len = exercises + .iter() + .map(|exercise| exercise.name.len()) + .max() + .unwrap_or(4) as u16; + + let widths = [ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Length(max_name_len), + Constraint::Fill(1), + ]; + + let rows = Self::rows(state, exercises); + + let table = Table::new(rows, widths) + .header(header) + .column_spacing(2) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) + .highlight_symbol("🦀") + .block(Block::default().borders(Borders::BOTTOM)); + + let selected = 0; + let table_state = TableState::default().with_selected(Some(selected)); + let last_ind = exercises.len() - 1; + + Self { + table, + selected, + table_state, + last_ind, + } + } + + fn select(&mut self, ind: usize) { + self.selected = ind; + self.table_state.select(Some(ind)); + } + + pub fn select_next(&mut self) { + self.select(self.selected.saturating_add(1).min(self.last_ind)); + } + + pub fn select_previous(&mut self) { + self.select(self.selected.saturating_sub(1)); + } + + #[inline] + pub fn select_first(&mut self) { + self.select(0); + } + + #[inline] + pub fn select_last(&mut self) { + self.select(self.last_ind); + } + + pub fn draw(&mut self, frame: &mut Frame) { + let area = frame.size(); + + frame.render_stateful_widget( + &self.table, + Rect { + x: 0, + y: 0, + width: area.width, + height: area.height - 1, + }, + &mut self.table_state, + ); + + // Help footer + let footer = + "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"; + frame.render_widget( + Span::raw(footer), + Rect { + x: 0, + y: area.height - 1, + width: area.width, + height: 1, + }, + ); + } } pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); - stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let mut table = table(state, exercises); - - let last_ind = exercises.len() - 1; - let mut selected = 0; - let mut table_state = TableState::default().with_selected(Some(selected)); + let mut ui_state = UiState::new(state, exercises); 'outer: loop { - terminal.draw(|frame| { - let area = frame.size(); - - frame.render_stateful_widget( - &table, - Rect { - x: 0, - y: 0, - width: area.width, - height: area.height - 1, - }, - &mut table_state, - ); - - // Help footer - frame.render_widget( - Span::raw("↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"), - Rect { - x: 0, - y: area.height - 1, - width: area.width, - height: 1, - }, - ); - })?; + terminal.draw(|frame| ui_state.draw(frame))?; let key = loop { match event::read()? { @@ -135,25 +179,13 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { match key.code { KeyCode::Char('q') => break, - KeyCode::Down | KeyCode::Char('j') => { - selected = selected.saturating_add(1).min(last_ind); - table_state.select(Some(selected)); - } - KeyCode::Up | KeyCode::Char('k') => { - selected = selected.saturating_sub(1).max(0); - table_state.select(Some(selected)); - } - KeyCode::Home | KeyCode::Char('g') => { - selected = 0; - table_state.select(Some(selected)); - } - KeyCode::End | KeyCode::Char('G') => { - selected = last_ind; - table_state.select(Some(selected)); - } + KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(), + KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), + KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), + KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), KeyCode::Char('c') => { - state.set_next_exercise_ind(selected)?; - table = table.rows(rows(state, exercises)); + state.set_next_exercise_ind(ui_state.selected)?; + ui_state.table = ui_state.table.rows(UiState::rows(state, exercises)); } _ => (), } From 8c31d38fa17970d0d2dc696922eb8cb329a6fdb9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 17:57:20 +0200 Subject: [PATCH 061/433] Better variable name --- src/list.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/list.rs b/src/list.rs index dad21822..cff0a3d8 100644 --- a/src/list.rs +++ b/src/list.rs @@ -133,11 +133,10 @@ impl<'a> UiState<'a> { &mut self.table_state, ); - // Help footer - let footer = + let help_footer = "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"; frame.render_widget( - Span::raw(footer), + Span::raw(help_footer), Rect { x: 0, y: area.height - 1, From 3bd26c7a24a97f9b4b87c453fbdbb06fe9971920 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 19:01:08 +0200 Subject: [PATCH 062/433] State -> StateFile --- src/list.rs | 20 ++++++++++---------- src/main.rs | 17 +++++++++-------- src/{state.rs => state_file.rs} | 4 ++-- src/watch.rs | 6 +++--- 4 files changed, 24 insertions(+), 23 deletions(-) rename src/{state.rs => state_file.rs} (97%) diff --git a/src/list.rs b/src/list.rs index cff0a3d8..c59b8d84 100644 --- a/src/list.rs +++ b/src/list.rs @@ -14,7 +14,7 @@ use ratatui::{ }; use std::io; -use crate::{exercise::Exercise, state::State}; +use crate::{exercise::Exercise, state_file::StateFile}; struct UiState<'a> { pub table: Table<'a>, @@ -25,7 +25,7 @@ struct UiState<'a> { impl<'a> UiState<'a> { pub fn rows<'s, 'i>( - state: &'s State, + state_file: &'s StateFile, exercises: &'a [Exercise], ) -> impl Iterator> + 'i where @@ -34,10 +34,10 @@ impl<'a> UiState<'a> { { exercises .iter() - .zip(state.progress()) + .zip(state_file.progress()) .enumerate() .map(|(ind, (exercise, done))| { - let next = if ind == state.next_exercise_ind() { + let next = if ind == state_file.next_exercise_ind() { ">>>>".bold().red() } else { Span::default() @@ -58,7 +58,7 @@ impl<'a> UiState<'a> { }) } - pub fn new(state: &State, exercises: &'a [Exercise]) -> Self { + pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); let max_name_len = exercises @@ -74,7 +74,7 @@ impl<'a> UiState<'a> { Constraint::Fill(1), ]; - let rows = Self::rows(state, exercises); + let rows = Self::rows(state_file, exercises); let table = Table::new(rows, widths) .header(header) @@ -147,7 +147,7 @@ impl<'a> UiState<'a> { } } -pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { +pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; @@ -155,7 +155,7 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let mut ui_state = UiState::new(state, exercises); + let mut ui_state = UiState::new(state_file, exercises); 'outer: loop { terminal.draw(|frame| ui_state.draw(frame))?; @@ -183,8 +183,8 @@ pub fn list(state: &mut State, exercises: &[Exercise]) -> Result<()> { KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), KeyCode::Char('c') => { - state.set_next_exercise_ind(ui_state.selected)?; - ui_state.table = ui_state.table.rows(UiState::rows(state, exercises)); + state_file.set_next_exercise_ind(ui_state.selected)?; + ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); } _ => (), } diff --git a/src/main.rs b/src/main.rs index e82fc808..3d691b08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,6 @@ -use crate::consts::WELCOME; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; -use crate::exercise::{Exercise, ExerciseList}; -use crate::run::run; -use crate::verify::verify; use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; -use state::State; +use state_file::StateFile; use std::path::Path; use std::process::exit; use verify::VerifyState; @@ -16,10 +11,16 @@ mod exercise; mod init; mod list; mod run; -mod state; +mod state_file; mod verify; mod watch; +use crate::consts::WELCOME; +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; +use crate::exercise::{Exercise, ExerciseList}; +use crate::run::run; +use crate::verify::verify; + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -85,7 +86,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let mut state = State::read_or_default(&exercises); + let mut state = StateFile::read_or_default(&exercises); match args.command { None | Some(Subcommands::Watch) => { diff --git a/src/state.rs b/src/state_file.rs similarity index 97% rename from src/state.rs rename to src/state_file.rs index 5a644873..ca7ed342 100644 --- a/src/state.rs +++ b/src/state_file.rs @@ -5,12 +5,12 @@ use std::fs; use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] -pub struct State { +pub struct StateFile { next_exercise_ind: usize, progress: Vec, } -impl State { +impl StateFile { fn read(exercises: &[Exercise]) -> Option { let file_content = fs::read(".rustlings.json").ok()?; diff --git a/src/watch.rs b/src/watch.rs index cc9668d0..1503fdfe 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -18,7 +18,7 @@ use std::{ use crate::{ exercise::{self, Exercise}, - state::State, + state_file::StateFile, }; enum Event { @@ -151,14 +151,14 @@ You can keep working on this exercise or jump into the next one by removing the } } -pub fn watch(state: &State, exercises: &[Exercise]) -> Result<()> { +pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; debouncer .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state.next_exercise_ind(); + let current_exercise_ind = state_file.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; From 0a674a158da0d519f03a88bfabf31d98c0e064c6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 19:05:29 +0200 Subject: [PATCH 063/433] Separate UiState --- src/list.rs | 144 ++------------------------------------------- src/list/state.rs | 145 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 139 deletions(-) create mode 100644 src/list/state.rs diff --git a/src/list.rs b/src/list.rs index c59b8d84..4d26702d 100644 --- a/src/list.rs +++ b/src/list.rs @@ -4,148 +4,14 @@ use crossterm::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; -use ratatui::{ - backend::CrosstermBackend, - layout::{Constraint, Rect}, - style::{Style, Stylize}, - text::Span, - widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, - Frame, Terminal, -}; +use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; +mod state; + use crate::{exercise::Exercise, state_file::StateFile}; -struct UiState<'a> { - pub table: Table<'a>, - selected: usize, - table_state: TableState, - last_ind: usize, -} - -impl<'a> UiState<'a> { - pub fn rows<'s, 'i>( - state_file: &'s StateFile, - exercises: &'a [Exercise], - ) -> impl Iterator> + 'i - where - 's: 'i, - 'a: 'i, - { - exercises - .iter() - .zip(state_file.progress()) - .enumerate() - .map(|(ind, (exercise, done))| { - let next = if ind == state_file.next_exercise_ind() { - ">>>>".bold().red() - } else { - Span::default() - }; - - let exercise_state = if *done { - "DONE".green() - } else { - "PENDING".yellow() - }; - - Row::new([ - next, - exercise_state, - Span::raw(&exercise.name), - Span::raw(exercise.path.to_string_lossy()), - ]) - }) - } - - pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { - let header = Row::new(["Next", "State", "Name", "Path"]); - - let max_name_len = exercises - .iter() - .map(|exercise| exercise.name.len()) - .max() - .unwrap_or(4) as u16; - - let widths = [ - Constraint::Length(4), - Constraint::Length(7), - Constraint::Length(max_name_len), - Constraint::Fill(1), - ]; - - let rows = Self::rows(state_file, exercises); - - let table = Table::new(rows, widths) - .header(header) - .column_spacing(2) - .highlight_spacing(HighlightSpacing::Always) - .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) - .highlight_symbol("🦀") - .block(Block::default().borders(Borders::BOTTOM)); - - let selected = 0; - let table_state = TableState::default().with_selected(Some(selected)); - let last_ind = exercises.len() - 1; - - Self { - table, - selected, - table_state, - last_ind, - } - } - - fn select(&mut self, ind: usize) { - self.selected = ind; - self.table_state.select(Some(ind)); - } - - pub fn select_next(&mut self) { - self.select(self.selected.saturating_add(1).min(self.last_ind)); - } - - pub fn select_previous(&mut self) { - self.select(self.selected.saturating_sub(1)); - } - - #[inline] - pub fn select_first(&mut self) { - self.select(0); - } - - #[inline] - pub fn select_last(&mut self) { - self.select(self.last_ind); - } - - pub fn draw(&mut self, frame: &mut Frame) { - let area = frame.size(); - - frame.render_stateful_widget( - &self.table, - Rect { - x: 0, - y: 0, - width: area.width, - height: area.height - 1, - }, - &mut self.table_state, - ); - - let help_footer = - "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"; - frame.render_widget( - Span::raw(help_footer), - Rect { - x: 0, - y: area.height - 1, - width: area.width, - height: 1, - }, - ); - } -} +use self::state::UiState; pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); @@ -183,7 +49,7 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), KeyCode::Char('c') => { - state_file.set_next_exercise_ind(ui_state.selected)?; + state_file.set_next_exercise_ind(ui_state.selected())?; ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); } _ => (), diff --git a/src/list/state.rs b/src/list/state.rs new file mode 100644 index 00000000..3d2f0a62 --- /dev/null +++ b/src/list/state.rs @@ -0,0 +1,145 @@ +use ratatui::{ + layout::{Constraint, Rect}, + style::{Style, Stylize}, + text::Span, + widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, + Frame, +}; + +use crate::{exercise::Exercise, state_file::StateFile}; + +pub struct UiState<'a> { + pub table: Table<'a>, + selected: usize, + table_state: TableState, + last_ind: usize, +} + +impl<'a> UiState<'a> { + pub fn rows<'s, 'i>( + state_file: &'s StateFile, + exercises: &'a [Exercise], + ) -> impl Iterator> + 'i + where + 's: 'i, + 'a: 'i, + { + exercises + .iter() + .zip(state_file.progress()) + .enumerate() + .map(|(ind, (exercise, done))| { + let next = if ind == state_file.next_exercise_ind() { + ">>>>".bold().red() + } else { + Span::default() + }; + + let exercise_state = if *done { + "DONE".green() + } else { + "PENDING".yellow() + }; + + Row::new([ + next, + exercise_state, + Span::raw(&exercise.name), + Span::raw(exercise.path.to_string_lossy()), + ]) + }) + } + + pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { + let header = Row::new(["Next", "State", "Name", "Path"]); + + let max_name_len = exercises + .iter() + .map(|exercise| exercise.name.len()) + .max() + .unwrap_or(4) as u16; + + let widths = [ + Constraint::Length(4), + Constraint::Length(7), + Constraint::Length(max_name_len), + Constraint::Fill(1), + ]; + + let rows = Self::rows(state_file, exercises); + + let table = Table::new(rows, widths) + .header(header) + .column_spacing(2) + .highlight_spacing(HighlightSpacing::Always) + .highlight_style(Style::new().bg(ratatui::style::Color::Rgb(50, 50, 50))) + .highlight_symbol("🦀") + .block(Block::default().borders(Borders::BOTTOM)); + + let selected = 0; + let table_state = TableState::default().with_selected(Some(selected)); + let last_ind = exercises.len() - 1; + + Self { + table, + selected, + table_state, + last_ind, + } + } + + #[inline] + pub fn selected(&self) -> usize { + self.selected + } + + fn select(&mut self, ind: usize) { + self.selected = ind; + self.table_state.select(Some(ind)); + } + + pub fn select_next(&mut self) { + self.select(self.selected.saturating_add(1).min(self.last_ind)); + } + + pub fn select_previous(&mut self) { + self.select(self.selected.saturating_sub(1)); + } + + #[inline] + pub fn select_first(&mut self) { + self.select(0); + } + + #[inline] + pub fn select_last(&mut self) { + self.select(self.last_ind); + } + + pub fn draw(&mut self, frame: &mut Frame) { + let area = frame.size(); + + frame.render_stateful_widget( + &self.table, + Rect { + x: 0, + y: 0, + width: area.width, + height: area.height - 1, + }, + &mut self.table_state, + ); + + let help_footer = + "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"; + frame.render_widget( + Span::raw(help_footer), + Rect { + x: 0, + y: area.height - 1, + width: area.width, + height: 1, + }, + ); + } +} From 9a4ee47c527251fc3efacacc31bd0e73ef527969 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 19:29:16 +0200 Subject: [PATCH 064/433] Separate WatchState --- src/watch.rs | 181 +++---------------------------------------- src/watch/state.rs | 186 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 169 deletions(-) create mode 100644 src/watch/state.rs diff --git a/src/watch.rs b/src/watch.rs index 1503fdfe..967f98c1 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,25 +1,18 @@ use anyhow::Result; -use crossterm::{ - style::{Attribute, ContentStyle, Stylize}, - terminal::{Clear, ClearType}, - ExecutableCommand, -}; -use notify_debouncer_mini::{ - new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, -}; +use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; use std::{ - fmt::Write as _, - io::{self, BufRead, StdoutLock, Write}, + io::{self, BufRead, Write}, path::Path, - sync::mpsc::{channel, sync_channel, Receiver}, + sync::mpsc::{channel, sync_channel}, thread, time::Duration, }; -use crate::{ - exercise::{self, Exercise}, - state_file::StateFile, -}; +mod state; + +use crate::{exercise::Exercise, state_file::StateFile}; + +use self::state::WatchState; enum Event { Hint, @@ -27,130 +20,6 @@ enum Event { Quit, } -struct WatchState<'a> { - writer: StdoutLock<'a>, - rx: Receiver, - exercises: &'a [Exercise], - exercise: &'a Exercise, - current_exercise_ind: usize, - stdout: Option>, - stderr: Option>, - message: Option, - prompt: Vec, -} - -impl<'a> WatchState<'a> { - fn run_exercise(&mut self) -> Result { - let output = self.exercise.run()?; - - if !output.status.success() { - self.stdout = Some(output.stdout); - self.stderr = Some(output.stderr); - return Ok(false); - } - - if let exercise::State::Pending(context) = self.exercise.state()? { - let mut message = format!( - " -You can keep working on this exercise or jump into the next one by removing the {} comment: - -", - "`I AM NOT DONE`".bold(), - ); - - for context_line in context { - let formatted_line = if context_line.important { - context_line.line.bold() - } else { - context_line.line.stylize() - }; - - writeln!( - message, - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - )?; - } - - self.stdout = Some(output.stdout); - self.message = Some(message); - return Ok(false); - } - - Ok(true) - } - - fn try_recv_event(&mut self) -> Result<()> { - let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { - return Ok(()); - }; - - if let Some(current_exercise_ind) = events? - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - { - self.current_exercise_ind = current_exercise_ind; - } else { - return Ok(()); - }; - - while self.current_exercise_ind < self.exercises.len() { - self.exercise = &self.exercises[self.current_exercise_ind]; - if !self.run_exercise()? { - break; - } - - self.current_exercise_ind += 1; - } - - Ok(()) - } - - fn prompt(&mut self) -> io::Result<()> { - self.writer.write_all(&self.prompt)?; - self.writer.flush() - } - - fn render(&mut self) -> Result<()> { - self.writer.execute(Clear(ClearType::All))?; - - if let Some(stdout) = &self.stdout { - self.writer.write_all(stdout)?; - } - - if let Some(stderr) = &self.stderr { - self.writer.write_all(stderr)?; - } - - if let Some(message) = &self.message { - self.writer.write_all(message.as_bytes())?; - } - - self.prompt()?; - - Ok(()) - } -} - pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { let (tx, rx) = channel(); let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; @@ -158,29 +27,7 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let current_exercise_ind = state_file.next_exercise_ind(); - - let exercise = &exercises[current_exercise_ind]; - - let writer = io::stdout().lock(); - - let mut watch_state = WatchState { - writer, - rx, - exercises, - exercise, - current_exercise_ind, - stdout: None, - stderr: None, - message: None, - prompt: format!( - "\n\n{}int/{}lear/{}uit? ", - "h".bold(), - "c".bold(), - "q".bold() - ) - .into_bytes(), - }; + let mut watch_state = WatchState::new(state_file, exercises, rx); watch_state.run_exercise()?; watch_state.render()?; @@ -214,24 +61,20 @@ pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { if let Ok(event) = rx.try_recv() { match event { Some(Event::Hint) => { - watch_state - .writer - .write_all(watch_state.exercise.hint.as_bytes())?; - watch_state.prompt()?; + watch_state.show_hint()?; } Some(Event::Clear) => { watch_state.render()?; } Some(Event::Quit) => break, None => { - watch_state.writer.write_all(b"Invalid command")?; - watch_state.prompt()?; + watch_state.handle_invalid_cmd()?; } } } } - watch_state.writer.write_all(b" + watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; diff --git a/src/watch/state.rs b/src/watch/state.rs new file mode 100644 index 00000000..40f48eff --- /dev/null +++ b/src/watch/state.rs @@ -0,0 +1,186 @@ +use anyhow::Result; +use crossterm::{ + style::{Attribute, ContentStyle, Stylize}, + terminal::{Clear, ClearType}, + ExecutableCommand, +}; +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::{ + fmt::Write as _, + io::{self, StdoutLock, Write as _}, + sync::mpsc::Receiver, + time::Duration, +}; + +use crate::{ + exercise::{Exercise, State}, + state_file::StateFile, +}; + +pub struct WatchState<'a> { + writer: StdoutLock<'a>, + rx: Receiver, + exercises: &'a [Exercise], + exercise: &'a Exercise, + current_exercise_ind: usize, + stdout: Option>, + stderr: Option>, + message: Option, + prompt: Vec, +} + +impl<'a> WatchState<'a> { + pub fn new( + state_file: &StateFile, + exercises: &'a [Exercise], + rx: Receiver, + ) -> Self { + let current_exercise_ind = state_file.next_exercise_ind(); + let exercise = &exercises[current_exercise_ind]; + + let writer = io::stdout().lock(); + + let prompt = format!( + "\n\n{}int/{}lear/{}uit? ", + "h".bold(), + "c".bold(), + "q".bold() + ) + .into_bytes(); + + Self { + writer, + rx, + exercises, + exercise, + current_exercise_ind, + stdout: None, + stderr: None, + message: None, + prompt, + } + } + + #[inline] + pub fn into_writer(self) -> StdoutLock<'a> { + self.writer + } + + pub fn run_exercise(&mut self) -> Result { + let output = self.exercise.run()?; + + if !output.status.success() { + self.stdout = Some(output.stdout); + self.stderr = Some(output.stderr); + return Ok(false); + } + + if let State::Pending(context) = self.exercise.state()? { + let mut message = format!( + " +You can keep working on this exercise or jump into the next one by removing the {} comment: + +", + "`I AM NOT DONE`".bold(), + ); + + for context_line in context { + let formatted_line = if context_line.important { + context_line.line.bold() + } else { + context_line.line.stylize() + }; + + writeln!( + message, + "{:>2} {} {}", + ContentStyle { + foreground_color: Some(crossterm::style::Color::Blue), + background_color: None, + underline_color: None, + attributes: Attribute::Bold.into() + } + .apply(context_line.number), + "|".blue(), + formatted_line, + )?; + } + + self.stdout = Some(output.stdout); + self.message = Some(message); + return Ok(false); + } + + Ok(true) + } + + pub fn try_recv_event(&mut self) -> Result<()> { + let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { + return Ok(()); + }; + + if let Some(current_exercise_ind) = events? + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + { + self.current_exercise_ind = current_exercise_ind; + } else { + return Ok(()); + }; + + while self.current_exercise_ind < self.exercises.len() { + self.exercise = &self.exercises[self.current_exercise_ind]; + if !self.run_exercise()? { + break; + } + + self.current_exercise_ind += 1; + } + + Ok(()) + } + + pub fn show_prompt(&mut self) -> io::Result<()> { + self.writer.write_all(&self.prompt)?; + self.writer.flush() + } + + pub fn render(&mut self) -> io::Result<()> { + self.writer.execute(Clear(ClearType::All))?; + + if let Some(stdout) = &self.stdout { + self.writer.write_all(stdout)?; + } + + if let Some(stderr) = &self.stderr { + self.writer.write_all(stderr)?; + } + + if let Some(message) = &self.message { + self.writer.write_all(message.as_bytes())?; + } + + self.show_prompt() + } + + pub fn show_hint(&mut self) -> io::Result<()> { + self.writer.write_all(self.exercise.hint.as_bytes())?; + self.show_prompt() + } + + pub fn handle_invalid_cmd(&mut self) -> io::Result<()> { + self.writer.write_all(b"Invalid command")?; + self.show_prompt() + } +} From db43efe3ec9d0bba5ee997923d68d2356b08a257 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 22:40:50 +0200 Subject: [PATCH 065/433] Update .gitignore --- .gitignore | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 0ea1fb6d..2d4a04dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,27 @@ +# Cargo target/ /tests/fixture/*/Cargo.lock /dev/Cargo.lock -*.swp -**/*.rs.bk +# State file +.rustlings-state.json + +# oranda +public/ +.netlify + +# OS .DS_Store -*.pdb +.direnv/ + +# Editor +*.swp .idea +*.iml + +# VS Code extension recommendations .vscode/* !.vscode/extensions.json -*.iml -*.o -public/ -.direnv/ -.ignore -# Local Netlify folder -.netlify +# Ignore file for editors like Helix +.ignore From 99c9ab467b3e57f9dca080a6fe9c1dbd991a3fdb Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 22:43:59 +0200 Subject: [PATCH 066/433] Implement resetting --- src/exercise.rs | 8 ++++++- src/list.rs | 6 +++++ src/main.rs | 57 +++++++++++++++++++++++------------------------ src/state_file.rs | 15 ++++++++++--- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index d01d427a..508f4776 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -10,7 +10,7 @@ use winnow::ascii::{space0, Caseless}; use winnow::combinator::opt; use winnow::Parser; -use crate::embedded::EMBEDDED_FILES; +use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; // The number of context lines above and below a highlighted line. const CONTEXT: usize = 2; @@ -220,6 +220,12 @@ impl Exercise { pub fn looks_done(&self) -> Result { self.state().map(|state| state == State::Done) } + + pub fn reset(&self) -> Result<()> { + EMBEDDED_FILES + .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite) + .with_context(|| format!("Failed to reset the exercise {self}")) + } } impl Display for Exercise { diff --git a/src/list.rs b/src/list.rs index 4d26702d..e2af21d3 100644 --- a/src/list.rs +++ b/src/list.rs @@ -48,6 +48,12 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), + KeyCode::Char('r') => { + let selected = ui_state.selected(); + exercises[selected].reset()?; + state_file.reset(selected)?; + ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); + } KeyCode::Char('c') => { state_file.set_next_exercise_ind(ui_state.selected())?; ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); diff --git a/src/main.rs b/src/main.rs index 3d691b08..81f66175 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ mod verify; mod watch; use crate::consts::WELCOME; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::exercise::{Exercise, ExerciseList}; use crate::run::run; use crate::verify::verify; @@ -56,6 +55,26 @@ enum Subcommands { List, } +fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<(usize, &'a Exercise)> { + if name == "next" { + for (ind, exercise) in exercises.iter().enumerate() { + if !exercise.looks_done()? { + return Ok((ind, exercise)); + } + } + + println!("🎉 Congratulations! You have done all the exercises!"); + println!("🔚 There are no more exercises to do next!"); + exit(0); + } + + exercises + .iter() + .enumerate() + .find(|(_, exercise)| exercise.name == name) + .with_context(|| format!("No exercise found for '{name}'!")) +} + fn main() -> Result<()> { let args = Args::parse(); @@ -86,30 +105,29 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let mut state = StateFile::read_or_default(&exercises); + let mut state_file = StateFile::read_or_default(&exercises); match args.command { None | Some(Subcommands::Watch) => { - watch::watch(&state, &exercises)?; + watch::watch(&state_file, &exercises)?; } // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::List) => { - list::list(&mut state, &exercises)?; + list::list(&mut state_file, &exercises)?; } Some(Subcommands::Run { name }) => { - let exercise = find_exercise(&name, &exercises)?; + let (_, exercise) = find_exercise(&name, &exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); } Some(Subcommands::Reset { name }) => { - let exercise = find_exercise(&name, &exercises)?; - EMBEDDED_FILES - .write_exercise_to_disk(&exercise.path, WriteStrategy::Overwrite) - .with_context(|| format!("Failed to reset the exercise {exercise}"))?; + let (ind, exercise) = find_exercise(&name, &exercises)?; + exercise.reset()?; + state_file.reset(ind)?; println!("The file {} has been reset!", exercise.path.display()); } Some(Subcommands::Hint { name }) => { - let exercise = find_exercise(&name, &exercises)?; + let (_, exercise) = find_exercise(&name, &exercises)?; println!("{}", exercise.hint); } Some(Subcommands::Verify) => match verify(&exercises, 0)? { @@ -120,22 +138,3 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Ok(()) } - -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<&'a Exercise> { - if name == "next" { - for exercise in exercises { - if !exercise.looks_done()? { - return Ok(exercise); - } - } - - println!("🎉 Congratulations! You have done all the exercises!"); - println!("🔚 There are no more exercises to do next!"); - exit(0); - } - - exercises - .iter() - .find(|e| e.name == name) - .with_context(|| format!("No exercise found for '{name}'!")) -} diff --git a/src/state_file.rs b/src/state_file.rs index ca7ed342..693c78dc 100644 --- a/src/state_file.rs +++ b/src/state_file.rs @@ -10,9 +10,11 @@ pub struct StateFile { progress: Vec, } +const BAD_INDEX_ERR: &str = "The next exercise index is higher than the number of exercises"; + impl StateFile { fn read(exercises: &[Exercise]) -> Option { - let file_content = fs::read(".rustlings.json").ok()?; + let file_content = fs::read(".rustlings-state.json").ok()?; let slf: Self = serde_json::de::from_slice(&file_content).ok()?; @@ -34,6 +36,8 @@ impl StateFile { // TODO: Capacity let mut buf = Vec::with_capacity(1024); serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; + fs::write(".rustlings-state.json", buf) + .context("Failed to write the state file `.rustlings-state.json`")?; Ok(()) } @@ -45,9 +49,8 @@ impl StateFile { pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { if ind >= self.progress.len() { - bail!("The next exercise index is higher than the number of exercises"); + bail!(BAD_INDEX_ERR); } - self.next_exercise_ind = ind; self.write() } @@ -56,4 +59,10 @@ impl StateFile { pub fn progress(&self) -> &[bool] { &self.progress } + + pub fn reset(&mut self, ind: usize) -> Result<()> { + let done = self.progress.get_mut(ind).context(BAD_INDEX_ERR)?; + *done = false; + self.write() + } } From 93f8d1610d293e57fd5002a9755c1f91a31ba891 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 23:37:40 +0200 Subject: [PATCH 067/433] Some renamings --- src/exercise.rs | 4 ++-- src/init.rs | 2 +- src/main.rs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 508f4776..ae47d5e6 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -41,11 +41,11 @@ pub enum Mode { } #[derive(Deserialize)] -pub struct ExerciseList { +pub struct InfoFile { pub exercises: Vec, } -impl ExerciseList { +impl InfoFile { pub fn parse() -> Result { // Read a local `info.toml` if it exists. // Mainly to let the tests work for now. diff --git a/src/init.rs b/src/init.rs index 6af32351..df2d19d8 100644 --- a/src/init.rs +++ b/src/init.rs @@ -56,7 +56,7 @@ fn create_vscode_dir() -> Result<()> { Ok(()) } -pub fn init_rustlings(exercises: &[Exercise]) -> Result<()> { +pub fn init(exercises: &[Exercise]) -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { bail!( "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist diff --git a/src/main.rs b/src/main.rs index 81f66175..3f10a8bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ mod verify; mod watch; use crate::consts::WELCOME; -use crate::exercise::{Exercise, ExerciseList}; +use crate::exercise::{Exercise, InfoFile}; use crate::run::run; use crate::verify::verify; @@ -84,10 +84,10 @@ Did you already install Rust? Try running `cargo --version` to diagnose the problem.", )?; - let exercises = ExerciseList::parse()?.exercises; + let exercises = InfoFile::parse()?.exercises; if matches!(args.command, Some(Subcommands::Init)) { - init::init_rustlings(&exercises).context("Initialization failed")?; + init::init(&exercises).context("Initialization failed")?; println!( "\nDone initialization!\n Run `cd rustlings` to go into the generated directory. From db25cc91576a05b02edd3754df85eb5668cec83f Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 23:54:32 +0200 Subject: [PATCH 068/433] Ignore .rustlings-state.json --- src/init.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index df2d19d8..bc561eaf 100644 --- a/src/init.rs +++ b/src/init.rs @@ -36,7 +36,8 @@ publish = false } fn create_gitignore() -> io::Result<()> { - let gitignore = b"/target"; + let gitignore = b"/target +/.rustlings-state.json"; OpenOptions::new() .create_new(true) .write(true) From 394ca402a8883581dc040546b4ca18b07d76a7f2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 7 Apr 2024 23:57:54 +0200 Subject: [PATCH 069/433] Remove the info_toml_content field --- rustlings-macros/src/lib.rs | 1 - src/embedded.rs | 1 - src/exercise.rs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index 598b5c35..d8da666e 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -75,7 +75,6 @@ pub fn include_files(_: TokenStream) -> TokenStream { quote! { EmbeddedFiles { - info_toml_content: ::std::include_str!("../info.toml"), exercises_dir: ExercisesDir { readme: EmbeddedFile { path: "exercises/README.md", diff --git a/src/embedded.rs b/src/embedded.rs index 56b4b618..1e2d6770 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -65,7 +65,6 @@ struct ExercisesDir { } pub struct EmbeddedFiles { - pub info_toml_content: &'static str, exercises_dir: ExercisesDir, } diff --git a/src/exercise.rs b/src/exercise.rs index ae47d5e6..c9fb3312 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -52,7 +52,7 @@ impl InfoFile { if let Ok(file_content) = fs::read_to_string("info.toml") { toml_edit::de::from_str(&file_content) } else { - toml_edit::de::from_str(EMBEDDED_FILES.info_toml_content) + toml_edit::de::from_str(include_str!("../info.toml")) } .context("Failed to parse `info.toml`") } From 3a4f2bebb487f3fef9ce222674eede86722824b3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 00:35:51 +0200 Subject: [PATCH 070/433] Remove test because of defaulting to watch mode --- tests/integration_tests.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index ccdd910e..2219fea4 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,14 +1,7 @@ use assert_cmd::prelude::*; use glob::glob; use predicates::boolean::PredicateBooleanExt; -use std::fs::File; -use std::io::Read; -use std::process::Command; - -#[test] -fn runs_without_arguments() { - Command::cargo_bin("rustlings").unwrap().assert().success(); -} +use std::{fs::File, io::Read, process::Command}; #[test] fn fails_when_in_wrong_dir() { From c2501ae733f27cf3d9f14cf1b14e437c8675d80c Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 00:36:10 +0200 Subject: [PATCH 071/433] Remove list tests because of the TUI --- tests/integration_tests.rs | 54 -------------------------------------- 1 file changed, 54 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2219fea4..f8f4383f 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -194,57 +194,3 @@ fn run_single_test_success_with_output() { .code(0) .stdout(predicates::str::contains("THIS TEST TOO SHALL PASS")); } - -#[test] -fn run_rustlings_list() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list"]) - .current_dir("tests/fixture/success") - .assert() - .success(); -} - -#[test] -fn run_rustlings_list_no_pending() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list"]) - .current_dir("tests/fixture/success") - .assert() - .success() - .stdout(predicates::str::contains("Pending").not()); -} - -#[test] -fn run_rustlings_list_both_done_and_pending() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list"]) - .current_dir("tests/fixture/state") - .assert() - .success() - .stdout(predicates::str::contains("Done").and(predicates::str::contains("Pending"))); -} - -#[test] -fn run_rustlings_list_without_pending() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list", "--solved"]) - .current_dir("tests/fixture/state") - .assert() - .success() - .stdout(predicates::str::contains("Pending").not()); -} - -#[test] -fn run_rustlings_list_without_done() { - Command::cargo_bin("rustlings") - .unwrap() - .args(["list", "--unsolved"]) - .current_dir("tests/fixture/state") - .assert() - .success() - .stdout(predicates::str::contains("Done").not()); -} From 25e855a009c47d30bfa4da93a93d8390df20fe45 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 00:36:26 +0200 Subject: [PATCH 072/433] Merge imports --- src/exercise.rs | 23 ++++++++++++++--------- src/main.rs | 16 ++++++++-------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index c9fb3312..232d7f95 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,14 +1,19 @@ use anyhow::{Context, Result}; use serde::Deserialize; -use std::fmt::{self, Debug, Display, Formatter}; -use std::fs::{self, File}; -use std::io::{self, BufRead, BufReader}; -use std::path::PathBuf; -use std::process::{Command, Output}; -use std::{array, mem}; -use winnow::ascii::{space0, Caseless}; -use winnow::combinator::opt; -use winnow::Parser; +use std::{ + array, + fmt::{self, Debug, Display, Formatter}, + fs::{self, File}, + io::{self, BufRead, BufReader}, + mem, + path::PathBuf, + process::{Command, Output}, +}; +use winnow::{ + ascii::{space0, Caseless}, + combinator::opt, + Parser, +}; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; diff --git a/src/main.rs b/src/main.rs index 3f10a8bd..cba525a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,6 @@ use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; -use state_file::StateFile; -use std::path::Path; -use std::process::exit; -use verify::VerifyState; +use std::{path::Path, process::exit}; mod consts; mod embedded; @@ -15,10 +12,13 @@ mod state_file; mod verify; mod watch; -use crate::consts::WELCOME; -use crate::exercise::{Exercise, InfoFile}; -use crate::run::run; -use crate::verify::verify; +use self::{ + consts::WELCOME, + exercise::{Exercise, InfoFile}, + run::run, + state_file::StateFile, + verify::{verify, VerifyState}, +}; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] From bd5503a0d363384fb551f3e303d0376a08d50831 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 01:33:11 +0200 Subject: [PATCH 073/433] Show message on reset --- src/list.rs | 11 +++++++++-- src/list/state.rs | 18 +++++++++++------- src/main.rs | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/list.rs b/src/list.rs index e2af21d3..3d91b8ae 100644 --- a/src/list.rs +++ b/src/list.rs @@ -5,7 +5,7 @@ use crossterm::{ ExecutableCommand, }; use ratatui::{backend::CrosstermBackend, Terminal}; -use std::io; +use std::{fmt::Write, io}; mod state; @@ -42,6 +42,8 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { } }; + ui_state.message.clear(); + match key.code { KeyCode::Char('q') => break, KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(), @@ -50,9 +52,14 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), KeyCode::Char('r') => { let selected = ui_state.selected(); - exercises[selected].reset()?; + let exercise = &exercises[selected]; + exercise.reset()?; state_file.reset(selected)?; + ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); + ui_state + .message + .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; } KeyCode::Char('c') => { state_file.set_next_exercise_ind(ui_state.selected())?; diff --git a/src/list/state.rs b/src/list/state.rs index 3d2f0a62..534b5359 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -10,6 +10,7 @@ use crate::{exercise::Exercise, state_file::StateFile}; pub struct UiState<'a> { pub table: Table<'a>, + pub message: String, selected: usize, table_state: TableState, last_ind: usize, @@ -77,14 +78,13 @@ impl<'a> UiState<'a> { .block(Block::default().borders(Borders::BOTTOM)); let selected = 0; - let table_state = TableState::default().with_selected(Some(selected)); - let last_ind = exercises.len() - 1; Self { table, selected, - table_state, - last_ind, + table_state: TableState::default().with_selected(Some(selected)), + last_ind: exercises.len() - 1, + message: String::with_capacity(128), } } @@ -130,10 +130,14 @@ impl<'a> UiState<'a> { &mut self.table_state, ); - let help_footer = - "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit"; + let message = if self.message.is_empty() { + // Help footer. + "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit" + } else { + &self.message + }; frame.render_widget( - Span::raw(help_footer), + Span::raw(message), Rect { x: 0, y: area.height - 1, diff --git a/src/main.rs b/src/main.rs index cba525a1..f6c4c200 100644 --- a/src/main.rs +++ b/src/main.rs @@ -124,7 +124,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let (ind, exercise) = find_exercise(&name, &exercises)?; exercise.reset()?; state_file.reset(ind)?; - println!("The file {} has been reset!", exercise.path.display()); + println!("The exercise {exercise} has been reset!"); } Some(Subcommands::Hint { name }) => { let (_, exercise) = find_exercise(&name, &exercises)?; From 0bf3f7e01f219372bea56e2c3e9144a1b76bd3af Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 01:34:41 +0200 Subject: [PATCH 074/433] Lowercase "filter" in help footer --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 534b5359..35a906a3 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -132,7 +132,7 @@ impl<'a> UiState<'a> { let message = if self.message.is_empty() { // Help footer. - "↓/j ↑/k home/g end/G │ Filter one/

ending │ eset │ ontinue at │ uit" + "↓/j ↑/k home/g end/G │ filter one/

ending │ eset │ ontinue at │ uit" } else { &self.message }; From 05729b27a06d50d4d3516c1b62a2c7450e4ac12a Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 01:49:38 +0200 Subject: [PATCH 075/433] Set a list offset --- src/list/state.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 35a906a3..d2ade97e 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -77,12 +77,15 @@ impl<'a> UiState<'a> { .highlight_symbol("🦀") .block(Block::default().borders(Borders::BOTTOM)); - let selected = 0; + let selected = state_file.next_exercise_ind(); + let table_state = TableState::default() + .with_offset(selected.saturating_sub(3)) + .with_selected(Some(selected)); Self { table, selected, - table_state: TableState::default().with_selected(Some(selected)), + table_state, last_ind: exercises.len() - 1, message: String::with_capacity(128), } From 7c4d33654fb37200905c06c198f427545fedd461 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 02:41:48 +0200 Subject: [PATCH 076/433] Implement done/pending filters --- src/list.rs | 30 +++++++++++++++++++++++++++--- src/list/state.rs | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/list.rs b/src/list.rs index 3d91b8ae..d7fa05f1 100644 --- a/src/list.rs +++ b/src/list.rs @@ -11,7 +11,7 @@ mod state; use crate::{exercise::Exercise, state_file::StateFile}; -use self::state::UiState; +use self::state::{Filter, UiState}; pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); @@ -50,20 +50,44 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), KeyCode::End | KeyCode::Char('G') => ui_state.select_last(), + KeyCode::Char('d') => { + let message = if ui_state.filter == Filter::Done { + ui_state.filter = Filter::None; + "Disabled filter DONE" + } else { + ui_state.filter = Filter::Done; + "Enabled filter DONE │ Press d again to disable the filter" + }; + + ui_state = ui_state.with_updated_rows(state_file); + ui_state.message.push_str(message); + } + KeyCode::Char('p') => { + let message = if ui_state.filter == Filter::Pending { + ui_state.filter = Filter::None; + "Disabled filter PENDING" + } else { + ui_state.filter = Filter::Pending; + "Enabled filter PENDING │ Press p again to disable the filter" + }; + + ui_state = ui_state.with_updated_rows(state_file); + ui_state.message.push_str(message); + } KeyCode::Char('r') => { let selected = ui_state.selected(); let exercise = &exercises[selected]; exercise.reset()?; state_file.reset(selected)?; - ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); + ui_state = ui_state.with_updated_rows(state_file); ui_state .message .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; } KeyCode::Char('c') => { state_file.set_next_exercise_ind(ui_state.selected())?; - ui_state.table = ui_state.table.rows(UiState::rows(state_file, exercises)); + ui_state = ui_state.with_updated_rows(state_file); } _ => (), } diff --git a/src/list/state.rs b/src/list/state.rs index d2ade97e..30567d1c 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -8,18 +8,28 @@ use ratatui::{ use crate::{exercise::Exercise, state_file::StateFile}; +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Filter { + Done, + Pending, + None, +} + pub struct UiState<'a> { pub table: Table<'a>, pub message: String, + pub filter: Filter, + exercises: &'a [Exercise], selected: usize, table_state: TableState, last_ind: usize, } impl<'a> UiState<'a> { - pub fn rows<'s, 'i>( + fn rows<'s, 'i>( state_file: &'s StateFile, exercises: &'a [Exercise], + filter: Filter, ) -> impl Iterator> + 'i where 's: 'i, @@ -27,30 +37,41 @@ impl<'a> UiState<'a> { { exercises .iter() - .zip(state_file.progress()) + .zip(state_file.progress().iter().copied()) .enumerate() - .map(|(ind, (exercise, done))| { + .filter_map(move |(ind, (exercise, done))| { + match (filter, done) { + (Filter::Done, false) | (Filter::Pending, true) => return None, + _ => (), + } + let next = if ind == state_file.next_exercise_ind() { ">>>>".bold().red() } else { Span::default() }; - let exercise_state = if *done { + let exercise_state = if done { "DONE".green() } else { "PENDING".yellow() }; - Row::new([ + Some(Row::new([ next, exercise_state, Span::raw(&exercise.name), Span::raw(exercise.path.to_string_lossy()), - ]) + ])) }) } + pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { + let rows = Self::rows(state_file, self.exercises, self.filter); + self.table = self.table.rows(rows); + self + } + pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); @@ -67,7 +88,8 @@ impl<'a> UiState<'a> { Constraint::Fill(1), ]; - let rows = Self::rows(state_file, exercises); + let filter = Filter::None; + let rows = Self::rows(state_file, exercises, filter); let table = Table::new(rows, widths) .header(header) @@ -84,10 +106,12 @@ impl<'a> UiState<'a> { Self { table, + message: String::with_capacity(128), + filter, + exercises, selected, table_state, last_ind: exercises.len() - 1, - message: String::with_capacity(128), } } From b5fc06bd56c6bf6a9b3d4e3dbcd4346c8256731c Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 02:46:35 +0200 Subject: [PATCH 077/433] Show more exercises before the selected one --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 30567d1c..48c90d28 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -101,7 +101,7 @@ impl<'a> UiState<'a> { let selected = state_file.next_exercise_ind(); let table_state = TableState::default() - .with_offset(selected.saturating_sub(3)) + .with_offset(selected.saturating_sub(10)) .with_selected(Some(selected)); Self { From 1db5de965305c0eb3f31e78217e8a52c61e15dd4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 03:08:05 +0200 Subject: [PATCH 078/433] Fix selection after applying filters --- src/list/state.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 48c90d28..902e7a6a 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -26,14 +26,16 @@ pub struct UiState<'a> { } impl<'a> UiState<'a> { - fn rows<'s, 'i>( + fn rows<'s, 'c, 'i>( state_file: &'s StateFile, exercises: &'a [Exercise], + rows_counter: &'c mut usize, filter: Filter, ) -> impl Iterator> + 'i where 's: 'i, 'a: 'i, + 'c: 'i, { exercises .iter() @@ -45,6 +47,8 @@ impl<'a> UiState<'a> { _ => (), } + *rows_counter += 1; + let next = if ind == state_file.next_exercise_ind() { ">>>>".bold().red() } else { @@ -67,8 +71,13 @@ impl<'a> UiState<'a> { } pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { - let rows = Self::rows(state_file, self.exercises, self.filter); + let mut rows_counter = 0; + let rows = Self::rows(state_file, self.exercises, &mut rows_counter, self.filter); self.table = self.table.rows(rows); + + self.last_ind = rows_counter.saturating_sub(1); + self.select(self.selected.min(self.last_ind)); + self } @@ -89,7 +98,8 @@ impl<'a> UiState<'a> { ]; let filter = Filter::None; - let rows = Self::rows(state_file, exercises, filter); + let mut rows_counter = 0; + let rows = Self::rows(state_file, exercises, &mut rows_counter, filter); let table = Table::new(rows, widths) .header(header) @@ -111,7 +121,7 @@ impl<'a> UiState<'a> { exercises, selected, table_state, - last_ind: exercises.len() - 1, + last_ind: rows_counter.saturating_sub(1), } } From 7c46e7ac697507ff1826bf5bf691a93898d4368d Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 03:16:38 +0200 Subject: [PATCH 079/433] Simplify building rows. No more lifetimes championship :( --- src/list/state.rs | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 902e7a6a..b3dbafed 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -26,28 +26,20 @@ pub struct UiState<'a> { } impl<'a> UiState<'a> { - fn rows<'s, 'c, 'i>( - state_file: &'s StateFile, - exercises: &'a [Exercise], - rows_counter: &'c mut usize, - filter: Filter, - ) -> impl Iterator> + 'i - where - 's: 'i, - 'a: 'i, - 'c: 'i, - { - exercises + pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { + let mut rows_counter: usize = 0; + let rows = self + .exercises .iter() .zip(state_file.progress().iter().copied()) .enumerate() - .filter_map(move |(ind, (exercise, done))| { - match (filter, done) { + .filter_map(|(ind, (exercise, done))| { + match (self.filter, done) { (Filter::Done, false) | (Filter::Pending, true) => return None, _ => (), } - *rows_counter += 1; + rows_counter += 1; let next = if ind == state_file.next_exercise_ind() { ">>>>".bold().red() @@ -67,12 +59,8 @@ impl<'a> UiState<'a> { Span::raw(&exercise.name), Span::raw(exercise.path.to_string_lossy()), ])) - }) - } + }); - pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { - let mut rows_counter = 0; - let rows = Self::rows(state_file, self.exercises, &mut rows_counter, self.filter); self.table = self.table.rows(rows); self.last_ind = rows_counter.saturating_sub(1); @@ -97,11 +85,8 @@ impl<'a> UiState<'a> { Constraint::Fill(1), ]; - let filter = Filter::None; - let mut rows_counter = 0; - let rows = Self::rows(state_file, exercises, &mut rows_counter, filter); - - let table = Table::new(rows, widths) + let table = Table::default() + .widths(widths) .header(header) .column_spacing(2) .highlight_spacing(HighlightSpacing::Always) @@ -114,15 +99,17 @@ impl<'a> UiState<'a> { .with_offset(selected.saturating_sub(10)) .with_selected(Some(selected)); - Self { + let slf = Self { table, message: String::with_capacity(128), - filter, + filter: Filter::None, exercises, selected, table_state, - last_ind: rows_counter.saturating_sub(1), - } + last_ind: 0, + }; + + slf.with_updated_rows(state_file) } #[inline] From d0fcd8ae8aac43e0c0ac933bd810f11fa79d962e Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 8 Apr 2024 03:21:13 +0200 Subject: [PATCH 080/433] Use a color for the message --- src/list/state.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index b3dbafed..dc9ff5fe 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -156,12 +156,14 @@ impl<'a> UiState<'a> { let message = if self.message.is_empty() { // Help footer. - "↓/j ↑/k home/g end/G │ filter one/

ending │ eset │ ontinue at │ uit" + Span::raw( + "↓/j ↑/k home/g end/G │ filter one/

ending │ eset │ ontinue at │ uit", + ) } else { - &self.message + self.message.as_str().blue() }; frame.render_widget( - Span::raw(message), + message, Rect { x: 0, y: area.height - 1, From ee7d9762832241b34dc5533bad4ed151e21acab1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 17:15:12 +0200 Subject: [PATCH 081/433] Use a green color on successful run --- src/run.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/run.rs b/src/run.rs index 38f4e0e2..2fd6f407 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Result}; +use crossterm::style::Stylize; use std::io::{stdout, Write}; use crate::exercise::Exercise; @@ -21,8 +22,7 @@ pub fn run(exercise: &Exercise) -> Result<()> { bail!("Ran {exercise} with errors"); } - // TODO: Color - println!("Successfully ran {exercise}"); + println!("{}", "✓ Successfully ran {exercise}".green()); Ok(()) } From 850c1d0234b2c1ae09a8f1c8f669e23a324fd644 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 19:37:39 +0200 Subject: [PATCH 082/433] Add progress bar to list --- src/list.rs | 2 +- src/list/state.rs | 56 +++++++++++++++++++++++++++++++++------------ src/main.rs | 1 + src/progress_bar.rs | 41 +++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 src/progress_bar.rs diff --git a/src/list.rs b/src/list.rs index d7fa05f1..db83ea4f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -24,7 +24,7 @@ pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { let mut ui_state = UiState::new(state_file, exercises); 'outer: loop { - terminal.draw(|frame| ui_state.draw(frame))?; + terminal.draw(|frame| ui_state.draw(frame).unwrap())?; let key = loop { match event::read()? { diff --git a/src/list/state.rs b/src/list/state.rs index dc9ff5fe..7bfc163a 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -1,12 +1,13 @@ +use anyhow::Result; use ratatui::{ layout::{Constraint, Rect}, style::{Style, Stylize}, text::Span, - widgets::{Block, Borders, HighlightSpacing, Row, Table, TableState}, + widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, Frame, }; -use crate::{exercise::Exercise, state_file::StateFile}; +use crate::{exercise::Exercise, progress_bar::progress_bar, state_file::StateFile}; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -20,6 +21,7 @@ pub struct UiState<'a> { pub message: String, pub filter: Filter, exercises: &'a [Exercise], + progress: u16, selected: usize, table_state: TableState, last_ind: usize, @@ -28,16 +30,28 @@ pub struct UiState<'a> { impl<'a> UiState<'a> { pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { let mut rows_counter: usize = 0; + let mut progress: u16 = 0; let rows = self .exercises .iter() .zip(state_file.progress().iter().copied()) .enumerate() .filter_map(|(ind, (exercise, done))| { - match (self.filter, done) { - (Filter::Done, false) | (Filter::Pending, true) => return None, - _ => (), - } + let exercise_state = if done { + progress += 1; + + if self.filter == Filter::Pending { + return None; + } + + "DONE".green() + } else { + if self.filter == Filter::Done { + return None; + } + + "PENDING".yellow() + }; rows_counter += 1; @@ -47,12 +61,6 @@ impl<'a> UiState<'a> { Span::default() }; - let exercise_state = if done { - "DONE".green() - } else { - "PENDING".yellow() - }; - Some(Row::new([ next, exercise_state, @@ -66,6 +74,8 @@ impl<'a> UiState<'a> { self.last_ind = rows_counter.saturating_sub(1); self.select(self.selected.min(self.last_ind)); + self.progress = progress; + self } @@ -104,6 +114,7 @@ impl<'a> UiState<'a> { message: String::with_capacity(128), filter: Filter::None, exercises, + progress: 0, selected, table_state, last_ind: 0, @@ -140,7 +151,7 @@ impl<'a> UiState<'a> { self.select(self.last_ind); } - pub fn draw(&mut self, frame: &mut Frame) { + pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { let area = frame.size(); frame.render_stateful_widget( @@ -149,11 +160,26 @@ impl<'a> UiState<'a> { x: 0, y: 0, width: area.width, - height: area.height - 1, + height: area.height - 3, }, &mut self.table_state, ); + frame.render_widget( + Paragraph::new(Span::raw(progress_bar( + self.progress, + self.exercises.len() as u16, + area.width, + )?)) + .block(Block::default().borders(Borders::BOTTOM)), + Rect { + x: 0, + y: area.height - 3, + width: area.width, + height: 2, + }, + ); + let message = if self.message.is_empty() { // Help footer. Span::raw( @@ -171,5 +197,7 @@ impl<'a> UiState<'a> { height: 1, }, ); + + Ok(()) } } diff --git a/src/main.rs b/src/main.rs index f6c4c200..356b77ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod embedded; mod exercise; mod init; mod list; +mod progress_bar; mod run; mod state_file; mod verify; diff --git a/src/progress_bar.rs b/src/progress_bar.rs new file mode 100644 index 00000000..b4abbfc9 --- /dev/null +++ b/src/progress_bar.rs @@ -0,0 +1,41 @@ +use anyhow::{bail, Result}; +use std::fmt::Write; + +pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { + if progress > total { + bail!("The progress of the progress bar is higher than the maximum"); + } + + // "Progress: [".len() == 11 + // "] xxx/xxx".len() == 9 + // 11 + 9 = 20 + let wrapper_width = 20; + + // If the line width is too low for a progress bar, just show the ratio. + if line_width < wrapper_width + 4 { + return Ok(format!("Progress: {progress}/{total}")); + } + + let mut line = String::with_capacity(usize::from(line_width)); + line.push_str("Progress: ["); + + let remaining_width = line_width.saturating_sub(wrapper_width); + let filled = (remaining_width * progress) / total; + + for _ in 0..filled { + line.push('='); + } + + if filled < remaining_width { + line.push('>'); + } + + for _ in 0..(remaining_width - filled).saturating_sub(1) { + line.push(' '); + } + + line.write_fmt(format_args!("] {progress:>3}/{total:<3}")) + .unwrap(); + + Ok(line) +} From f0ce2c1afa21fdaa34aed8f21c1ef4d3c47cebdd Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:07:53 +0200 Subject: [PATCH 083/433] Improve event handling in the watch mode --- src/main.rs | 5 +- src/watch.rs | 150 ++++++++++++++++++++++++++++++++------------- src/watch/state.rs | 73 ++++++++-------------- 3 files changed, 133 insertions(+), 95 deletions(-) diff --git a/src/main.rs b/src/main.rs index 356b77ca..6af66bd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,8 @@ Did you already install Rust? Try running `cargo --version` to diagnose the problem.", )?; - let exercises = InfoFile::parse()?.exercises; + // Leaking is not a problem since the exercises are used until the end of the program. + let exercises = InfoFile::parse()?.exercises.leak(); if matches!(args.command, Some(Subcommands::Init)) { init::init(&exercises).context("Initialization failed")?; @@ -110,7 +111,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini match args.command { None | Some(Subcommands::Watch) => { - watch::watch(&state_file, &exercises)?; + watch::watch(&state_file, exercises)?; } // `Init` is handled above. Some(Subcommands::Init) => (), diff --git a/src/watch.rs b/src/watch.rs index 967f98c1..abf40020 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,9 +1,11 @@ -use anyhow::Result; -use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; +use anyhow::{bail, Context, Result}; +use notify_debouncer_mini::{ + new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, +}; use std::{ io::{self, BufRead, Write}, path::Path, - sync::mpsc::{channel, sync_channel}, + sync::mpsc::{channel, Sender}, thread, time::Duration, }; @@ -14,70 +16,130 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::WatchState; -enum Event { +enum InputEvent { Hint, Clear, Quit, + Unrecognized, } -pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { +enum WatchEvent { + Input(InputEvent), + FileChange { exercise_ind: usize }, + TerminalResize, +} + +struct DebouceEventHandler { + tx: Sender, + exercises: &'static [Exercise], +} + +impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let Ok(event) = event else { + // TODO + return; + }; + + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; + + self.tx.send(WatchEvent::FileChange { exercise_ind }); + } +} + +fn input_handler(tx: Sender) -> Result<()> { + let mut stdin = io::stdin().lock(); + let mut stdin_buf = String::with_capacity(8); + + loop { + stdin + .read_line(&mut stdin_buf) + .context("Failed to read the user's input from stdin")?; + + let event = match stdin_buf.trim() { + "h" | "hint" => InputEvent::Hint, + "c" | "clear" => InputEvent::Clear, + "q" | "quit" => InputEvent::Quit, + _ => InputEvent::Unrecognized, + }; + + stdin_buf.clear(); + + if tx.send(WatchEvent::Input(event)).is_err() { + return Ok(()); + } + } +} + +pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> { let (tx, rx) = channel(); - let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; + let mut debouncer = new_debouncer( + Duration::from_secs(1), + DebouceEventHandler { + tx: tx.clone(), + exercises, + }, + )?; debouncer .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let mut watch_state = WatchState::new(state_file, exercises, rx); + let mut watch_state = WatchState::new(state_file, exercises); + // TODO: bool watch_state.run_exercise()?; watch_state.render()?; - let (tx, rx) = sync_channel(0); - thread::spawn(move || { - let mut stdin = io::stdin().lock(); - let mut stdin_buf = String::with_capacity(8); + let input_thread = thread::spawn(move || input_handler(tx)); - loop { - stdin.read_line(&mut stdin_buf).unwrap(); - - let event = match stdin_buf.trim() { - "h" | "hint" => Some(Event::Hint), - "c" | "clear" => Some(Event::Clear), - "q" | "quit" => Some(Event::Quit), - _ => None, - }; - - stdin_buf.clear(); - - if tx.send(event).is_err() { - break; - }; - } - }); - - loop { - watch_state.try_recv_event()?; - - if let Ok(event) = rx.try_recv() { - match event { - Some(Event::Hint) => { - watch_state.show_hint()?; - } - Some(Event::Clear) => { - watch_state.render()?; - } - Some(Event::Quit) => break, - None => { - watch_state.handle_invalid_cmd()?; - } + while let Ok(event) = rx.recv() { + match event { + WatchEvent::Input(InputEvent::Hint) => { + watch_state.show_hint()?; + } + WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { + watch_state.render()?; + } + WatchEvent::Input(InputEvent::Quit) => break, + WatchEvent::Input(InputEvent::Unrecognized) => { + watch_state.handle_invalid_cmd()?; + } + WatchEvent::FileChange { exercise_ind } => { + // TODO: bool + watch_state.run_exercise_with_ind(exercise_ind)?; + watch_state.render()?; } } } + // Drop the receiver for the sender threads to exit. + drop(rx); + watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; + match input_thread.join() { + Ok(res) => res?, + Err(_) => bail!("The input thread panicked"), + } + Ok(()) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 40f48eff..f614ae0d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,26 +1,23 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::{ style::{Attribute, ContentStyle, Stylize}, - terminal::{Clear, ClearType}, + terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use std::{ fmt::Write as _, io::{self, StdoutLock, Write as _}, - sync::mpsc::Receiver, - time::Duration, }; use crate::{ exercise::{Exercise, State}, + progress_bar::progress_bar, state_file::StateFile, }; pub struct WatchState<'a> { writer: StdoutLock<'a>, - rx: Receiver, - exercises: &'a [Exercise], + exercises: &'static [Exercise], exercise: &'a Exercise, current_exercise_ind: usize, stdout: Option>, @@ -30,11 +27,7 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn new( - state_file: &StateFile, - exercises: &'a [Exercise], - rx: Receiver, - ) -> Self { + pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { let current_exercise_ind = state_file.next_exercise_ind(); let exercise = &exercises[current_exercise_ind]; @@ -50,7 +43,6 @@ impl<'a> WatchState<'a> { Self { writer, - rx, exercises, exercise, current_exercise_ind, @@ -114,41 +106,14 @@ You can keep working on this exercise or jump into the next one by removing the Ok(true) } - pub fn try_recv_event(&mut self) -> Result<()> { - let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { - return Ok(()); - }; + pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result { + self.exercise = self + .exercises + .get(exercise_ind) + .context("Invalid exercise index")?; + self.current_exercise_ind = exercise_ind; - if let Some(current_exercise_ind) = events? - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - { - self.current_exercise_ind = current_exercise_ind; - } else { - return Ok(()); - }; - - while self.current_exercise_ind < self.exercises.len() { - self.exercise = &self.exercises[self.current_exercise_ind]; - if !self.run_exercise()? { - break; - } - - self.current_exercise_ind += 1; - } - - Ok(()) + self.run_exercise() } pub fn show_prompt(&mut self) -> io::Result<()> { @@ -156,7 +121,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.flush() } - pub fn render(&mut self) -> io::Result<()> { + pub fn render(&mut self) -> Result<()> { self.writer.execute(Clear(ClearType::All))?; if let Some(stdout) = &self.stdout { @@ -171,7 +136,17 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(message.as_bytes())?; } - self.show_prompt() + let line_width = size()?.0; + let progress_bar = progress_bar( + self.current_exercise_ind as u16, + self.exercises.len() as u16, + line_width, + )?; + self.writer.write_all(progress_bar.as_bytes())?; + + self.show_prompt()?; + + Ok(()) } pub fn show_hint(&mut self) -> io::Result<()> { From 787bec9875ec3e76d5870808cc7299da1d26dea6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:16:27 +0200 Subject: [PATCH 084/433] Use exercises as leaked --- src/list.rs | 2 +- src/list/state.rs | 10 +++++----- src/main.rs | 16 ++++++++-------- src/verify.rs | 9 ++++++--- src/watch/state.rs | 2 +- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/list.rs b/src/list.rs index db83ea4f..c92b3692 100644 --- a/src/list.rs +++ b/src/list.rs @@ -13,7 +13,7 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::{Filter, UiState}; -pub fn list(state_file: &mut StateFile, exercises: &[Exercise]) -> Result<()> { +pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; diff --git a/src/list/state.rs b/src/list/state.rs index 7bfc163a..b67c624b 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -16,18 +16,18 @@ pub enum Filter { None, } -pub struct UiState<'a> { - pub table: Table<'a>, +pub struct UiState { + pub table: Table<'static>, pub message: String, pub filter: Filter, - exercises: &'a [Exercise], + exercises: &'static [Exercise], progress: u16, selected: usize, table_state: TableState, last_ind: usize, } -impl<'a> UiState<'a> { +impl UiState { pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { let mut rows_counter: usize = 0; let mut progress: u16 = 0; @@ -79,7 +79,7 @@ impl<'a> UiState<'a> { self } - pub fn new(state_file: &StateFile, exercises: &'a [Exercise]) -> Self { + pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); let max_name_len = exercises diff --git a/src/main.rs b/src/main.rs index 6af66bd7..62bfd98b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ enum Subcommands { List, } -fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> Result<(usize, &'a Exercise)> { +fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> { if name == "next" { for (ind, exercise) in exercises.iter().enumerate() { if !exercise.looks_done()? { @@ -89,7 +89,7 @@ Try running `cargo --version` to diagnose the problem.", let exercises = InfoFile::parse()?.exercises.leak(); if matches!(args.command, Some(Subcommands::Init)) { - init::init(&exercises).context("Initialization failed")?; + init::init(exercises).context("Initialization failed")?; println!( "\nDone initialization!\n Run `cd rustlings` to go into the generated directory. @@ -107,7 +107,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let mut state_file = StateFile::read_or_default(&exercises); + let mut state_file = StateFile::read_or_default(exercises); match args.command { None | Some(Subcommands::Watch) => { @@ -116,23 +116,23 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::List) => { - list::list(&mut state_file, &exercises)?; + list::list(&mut state_file, exercises)?; } Some(Subcommands::Run { name }) => { - let (_, exercise) = find_exercise(&name, &exercises)?; + let (_, exercise) = find_exercise(&name, exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); } Some(Subcommands::Reset { name }) => { - let (ind, exercise) = find_exercise(&name, &exercises)?; + let (ind, exercise) = find_exercise(&name, exercises)?; exercise.reset()?; state_file.reset(ind)?; println!("The exercise {exercise} has been reset!"); } Some(Subcommands::Hint { name }) => { - let (_, exercise) = find_exercise(&name, &exercises)?; + let (_, exercise) = find_exercise(&name, exercises)?; println!("{}", exercise.hint); } - Some(Subcommands::Verify) => match verify(&exercises, 0)? { + Some(Subcommands::Verify) => match verify(exercises, 0)? { VerifyState::AllExercisesDone => println!("All exercises done!"), VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), }, diff --git a/src/verify.rs b/src/verify.rs index c4368cc7..cea6bdf6 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -4,9 +4,9 @@ use std::io::{stdout, Write}; use crate::exercise::{Exercise, Mode, State}; -pub enum VerifyState<'a> { +pub enum VerifyState { AllExercisesDone, - Failed(&'a Exercise), + Failed(&'static Exercise), } // Verify that the provided container of Exercise objects @@ -14,7 +14,10 @@ pub enum VerifyState<'a> { // Any such failures will be reported to the end user. // If the Exercise being verified is a test, the verbose boolean // determines whether or not the test harness outputs are displayed. -pub fn verify(exercises: &[Exercise], mut current_exercise_ind: usize) -> Result> { +pub fn verify( + exercises: &'static [Exercise], + mut current_exercise_ind: usize, +) -> Result { while current_exercise_ind < exercises.len() { let exercise = &exercises[current_exercise_ind]; diff --git a/src/watch/state.rs b/src/watch/state.rs index f614ae0d..d8fed5b7 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -18,7 +18,7 @@ use crate::{ pub struct WatchState<'a> { writer: StdoutLock<'a>, exercises: &'static [Exercise], - exercise: &'a Exercise, + exercise: &'static Exercise, current_exercise_ind: usize, stdout: Option>, stderr: Option>, From b15e0a279b17d29a3fa6408b76da35f0b843ce21 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:23:02 +0200 Subject: [PATCH 085/433] Use shrink to fit before leaking the vector --- src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 62bfd98b..504c02dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,8 +85,10 @@ Did you already install Rust? Try running `cargo --version` to diagnose the problem.", )?; - // Leaking is not a problem since the exercises are used until the end of the program. - let exercises = InfoFile::parse()?.exercises.leak(); + let mut info_file = InfoFile::parse()?; + info_file.exercises.shrink_to_fit(); + // Leaking is not a problem since the exercises' slice is used until the end of the program. + let exercises = info_file.exercises.leak(); if matches!(args.command, Some(Subcommands::Init)) { init::init(exercises).context("Initialization failed")?; From 4110ae21afd2c026e49d330918e212f4ab0eb5cc Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 21:46:55 +0200 Subject: [PATCH 086/433] Handle notify errors --- src/watch.rs | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index abf40020..5a1e38ab 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Result}; use notify_debouncer_mini::{ - new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind, + new_debouncer, + notify::{self, RecursiveMode}, + DebounceEventResult, DebouncedEventKind, }; use std::{ io::{self, BufRead, Write}, @@ -26,6 +28,7 @@ enum InputEvent { enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, + NotifyErr(notify::Error), TerminalResize, } @@ -36,30 +39,32 @@ struct DebouceEventHandler { impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { fn handle_event(&mut self, event: DebounceEventResult) { - let Ok(event) = event else { - // TODO - return; - }; - - let Some(exercise_ind) = event - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises + let event = match event { + Ok(event) => { + let Some(exercise_ind) = event .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - else { - return; + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), }; - self.tx.send(WatchEvent::FileChange { exercise_ind }); + let _ = self.tx.send(event); } } @@ -125,6 +130,7 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( watch_state.run_exercise_with_ind(exercise_ind)?; watch_state.render()?; } + WatchEvent::NotifyErr(e) => return Err(e.into()), } } From ff6c15f9c15ae80b48d3acd7091eb6328c931e7a Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:04:10 +0200 Subject: [PATCH 087/433] Don't try to join the input thread --- src/watch.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 5a1e38ab..6324eb36 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Result}; +use anyhow::Result; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, @@ -29,6 +29,7 @@ enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, NotifyErr(notify::Error), + StdinErr(io::Error), TerminalResize, } @@ -64,18 +65,23 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { Err(e) => WatchEvent::NotifyErr(e), }; + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. let _ = self.tx.send(event); } } -fn input_handler(tx: Sender) -> Result<()> { +fn input_handler(tx: Sender) { let mut stdin = io::stdin().lock(); let mut stdin_buf = String::with_capacity(8); loop { - stdin - .read_line(&mut stdin_buf) - .context("Failed to read the user's input from stdin")?; + if let Err(e) = stdin.read_line(&mut stdin_buf) { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::StdinErr(e)); + return; + } let event = match stdin_buf.trim() { "h" | "hint" => InputEvent::Hint, @@ -87,7 +93,8 @@ fn input_handler(tx: Sender) -> Result<()> { stdin_buf.clear(); if tx.send(WatchEvent::Input(event)).is_err() { - return Ok(()); + // The receiver was dropped. + return; } } } @@ -111,7 +118,7 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( watch_state.run_exercise()?; watch_state.render()?; - let input_thread = thread::spawn(move || input_handler(tx)); + thread::spawn(move || input_handler(tx)); while let Ok(event) = rx.recv() { match event { @@ -131,21 +138,14 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( watch_state.render()?; } WatchEvent::NotifyErr(e) => return Err(e.into()), + WatchEvent::StdinErr(e) => return Err(e.into()), } } - // Drop the receiver for the sender threads to exit. - drop(rx); - watch_state.into_writer().write_all(b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; - match input_thread.join() { - Ok(res) => res?, - Err(_) => bail!("The input thread panicked"), - } - Ok(()) } From af85f2036cd545013225da04e67257fe4f6a4179 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:06:55 +0200 Subject: [PATCH 088/433] Print a newline before the progress bar --- src/watch/state.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/watch/state.rs b/src/watch/state.rs index d8fed5b7..8fae7e87 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -136,6 +136,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(message.as_bytes())?; } + self.writer.write_all(b"\n")?; let line_width = size()?.0; let progress_bar = progress_bar( self.current_exercise_ind as u16, From a8ddc07a9aea5b2e3840a7b6e0eb20f2189bdd60 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:15:41 +0200 Subject: [PATCH 089/433] Add "exercises" to the end of the progress bar --- src/progress_bar.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/progress_bar.rs b/src/progress_bar.rs index b4abbfc9..ee55ba7b 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -7,13 +7,13 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result 99) + // 11 + 19 = 30 + let wrapper_width = 30; // If the line width is too low for a progress bar, just show the ratio. if line_width < wrapper_width + 4 { - return Ok(format!("Progress: {progress}/{total}")); + return Ok(format!("Progress: {progress}/{total} exercises")); } let mut line = String::with_capacity(usize::from(line_width)); @@ -34,7 +34,7 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result3}/{total:<3}")) + line.write_fmt(format_args!("] {progress:>3}/{total} exercises")) .unwrap(); Ok(line) From c8d217ad50a7117fe35735b4083f2aa1e2b47d97 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 9 Apr 2024 22:20:12 +0200 Subject: [PATCH 090/433] Fix showing stdout and stderr --- src/watch/state.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index 8fae7e87..24978bb7 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -60,13 +60,15 @@ impl<'a> WatchState<'a> { pub fn run_exercise(&mut self) -> Result { let output = self.exercise.run()?; + self.stdout = Some(output.stdout); if !output.status.success() { - self.stdout = Some(output.stdout); self.stderr = Some(output.stderr); return Ok(false); } + self.stderr = None; + if let State::Pending(context) = self.exercise.state()? { let mut message = format!( " @@ -98,7 +100,6 @@ You can keep working on this exercise or jump into the next one by removing the )?; } - self.stdout = Some(output.stdout); self.message = Some(message); return Ok(false); } From 4a80bf64411f228c35c173b6188df5114d4c52fa Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 00:42:32 +0200 Subject: [PATCH 091/433] Colorize the progress bar --- src/list/state.rs | 6 +-- src/progress_bar.rs | 96 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index b67c624b..89189794 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -7,7 +7,7 @@ use ratatui::{ Frame, }; -use crate::{exercise::Exercise, progress_bar::progress_bar, state_file::StateFile}; +use crate::{exercise::Exercise, progress_bar::progress_bar_ratatui, state_file::StateFile}; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -166,11 +166,11 @@ impl UiState { ); frame.render_widget( - Paragraph::new(Span::raw(progress_bar( + Paragraph::new(progress_bar_ratatui( self.progress, self.exercises.len() as u16, area.width, - )?)) + )?) .block(Block::default().borders(Borders::BOTTOM)), Rect { x: 0, diff --git a/src/progress_bar.rs b/src/progress_bar.rs index ee55ba7b..97c8ad90 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -1,41 +1,97 @@ use anyhow::{bail, Result}; +use ratatui::text::{Line, Span}; use std::fmt::Write; +const PREFIX: &str = "Progress: ["; +const PREFIX_WIDTH: u16 = PREFIX.len() as u16; +// Leaving the last char empty (_) for `total` > 99. +const POSTFIX_WIDTH: u16 = "] xxx/xx exercises_".len() as u16; +const WRAPPER_WIDTH: u16 = PREFIX_WIDTH + POSTFIX_WIDTH; +const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; + +const PROGRESS_EXCEEDS_MAX_ERR: &str = + "The progress of the progress bar is higher than the maximum"; + pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { + use crossterm::style::Stylize; + if progress > total { - bail!("The progress of the progress bar is higher than the maximum"); + bail!(PROGRESS_EXCEEDS_MAX_ERR); } - // "Progress: [".len() == 11 - // "] xxx/xx exercises_".len() == 19 (leaving the last char empty for `total` > 99) - // 11 + 19 = 30 - let wrapper_width = 30; - - // If the line width is too low for a progress bar, just show the ratio. - if line_width < wrapper_width + 4 { + if line_width < MIN_LINE_WIDTH { return Ok(format!("Progress: {progress}/{total} exercises")); } let mut line = String::with_capacity(usize::from(line_width)); - line.push_str("Progress: ["); + line.push_str(PREFIX); - let remaining_width = line_width.saturating_sub(wrapper_width); - let filled = (remaining_width * progress) / total; + let width = line_width - WRAPPER_WIDTH; + let filled = (width * progress) / total; + let mut green_part = String::with_capacity(usize::from(filled + 1)); for _ in 0..filled { - line.push('='); + green_part.push('#'); } - if filled < remaining_width { - line.push('>'); + if filled < width { + green_part.push('>'); + } + write!(line, "{}", green_part.green()).unwrap(); + + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + let red_part_width = width_minus_filled - 1; + let mut red_part = String::with_capacity(usize::from(red_part_width)); + for _ in 0..red_part_width { + red_part.push('-'); + } + write!(line, "{}", red_part.red()).unwrap(); } - for _ in 0..(remaining_width - filled).saturating_sub(1) { - line.push(' '); - } - - line.write_fmt(format_args!("] {progress:>3}/{total} exercises")) - .unwrap(); + write!(line, "] {progress:>3}/{total} exercises").unwrap(); Ok(line) } + +pub fn progress_bar_ratatui(progress: u16, total: u16, line_width: u16) -> Result> { + use ratatui::style::Stylize; + + if progress > total { + bail!(PROGRESS_EXCEEDS_MAX_ERR); + } + + if line_width < MIN_LINE_WIDTH { + return Ok(Line::raw(format!("Progress: {progress}/{total} exercises"))); + } + + let mut spans = Vec::with_capacity(4); + spans.push(Span::raw(PREFIX)); + + let width = line_width - WRAPPER_WIDTH; + let filled = (width * progress) / total; + + let mut green_part = String::with_capacity(usize::from(filled + 1)); + for _ in 0..filled { + green_part.push('#'); + } + + if filled < width { + green_part.push('>'); + } + spans.push(green_part.green()); + + let width_minus_filled = width - filled; + if width_minus_filled > 1 { + let red_part_width = width_minus_filled - 1; + let mut red_part = String::with_capacity(usize::from(red_part_width)); + for _ in 0..red_part_width { + red_part.push('-'); + } + spans.push(red_part.red()); + } + + spans.push(Span::raw(format!("] {progress:>3}/{total} exercises"))); + + Ok(Line::from(spans)) +} From 533a009257adba0714292d326f57671f77cffbd3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 00:51:41 +0200 Subject: [PATCH 092/433] Show the progress in the progress bar, not the current exercise index --- src/watch/state.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index 24978bb7..4db9440b 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -20,6 +20,7 @@ pub struct WatchState<'a> { exercises: &'static [Exercise], exercise: &'static Exercise, current_exercise_ind: usize, + progress: u16, stdout: Option>, stderr: Option>, message: Option, @@ -29,6 +30,7 @@ pub struct WatchState<'a> { impl<'a> WatchState<'a> { pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { let current_exercise_ind = state_file.next_exercise_ind(); + let progress = state_file.progress().iter().filter(|done| **done).count() as u16; let exercise = &exercises[current_exercise_ind]; let writer = io::stdout().lock(); @@ -46,6 +48,7 @@ impl<'a> WatchState<'a> { exercises, exercise, current_exercise_ind, + progress, stdout: None, stderr: None, message: None, @@ -139,11 +142,7 @@ You can keep working on this exercise or jump into the next one by removing the self.writer.write_all(b"\n")?; let line_width = size()?.0; - let progress_bar = progress_bar( - self.current_exercise_ind as u16, - self.exercises.len() as u16, - line_width, - )?; + let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; self.writer.write_all(progress_bar.as_bytes())?; self.show_prompt()?; From d1a965f019d0e8f22d5a57f0a7abd8cd4a8d0d0c Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 02:12:50 +0200 Subject: [PATCH 093/433] Make the list mode part of the watch mode --- src/main.rs | 19 +++++++++++-------- src/watch.rs | 27 +++++++++++++++++++++++---- src/watch/state.rs | 5 +++-- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 504c02dc..fc83e0fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,11 @@ mod watch; use self::{ consts::WELCOME, exercise::{Exercise, InfoFile}, + list::list, run::run, state_file::StateFile, verify::{verify, VerifyState}, + watch::{watch, WatchExit}, }; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code @@ -52,8 +54,6 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// List the exercises available in Rustlings - List, } fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> { @@ -112,14 +112,17 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let mut state_file = StateFile::read_or_default(exercises); match args.command { - None | Some(Subcommands::Watch) => { - watch::watch(&state_file, exercises)?; - } + None | Some(Subcommands::Watch) => loop { + match watch(&mut state_file, exercises)? { + WatchExit::Shutdown => break, + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list(&mut state_file, exercises)?, + } + }, // `Init` is handled above. Some(Subcommands::Init) => (), - Some(Subcommands::List) => { - list::list(&mut state_file, exercises)?; - } Some(Subcommands::Run { name }) => { let (_, exercise) = find_exercise(&name, exercises)?; run(exercise).unwrap_or_else(|_| exit(1)); diff --git a/src/watch.rs b/src/watch.rs index 6324eb36..004a13f6 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -18,9 +18,19 @@ use crate::{exercise::Exercise, state_file::StateFile}; use self::state::WatchState; +/// Returned by the watch mode to indicate what to do afterwards. +pub enum WatchExit { + /// Exit the program. + Shutdown, + /// Enter the list mode and restart the watch mode afterwards. + List, +} + +#[derive(Copy, Clone)] enum InputEvent { Hint, Clear, + List, Quit, Unrecognized, } @@ -86,20 +96,26 @@ fn input_handler(tx: Sender) { let event = match stdin_buf.trim() { "h" | "hint" => InputEvent::Hint, "c" | "clear" => InputEvent::Clear, + "l" | "list" => InputEvent::List, "q" | "quit" => InputEvent::Quit, _ => InputEvent::Unrecognized, }; - stdin_buf.clear(); - if tx.send(WatchEvent::Input(event)).is_err() { // The receiver was dropped. return; } + + match event { + InputEvent::List | InputEvent::Quit => return, + _ => (), + } + + stdin_buf.clear(); } } -pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> { +pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), @@ -125,6 +141,9 @@ pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<( WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; } + WatchEvent::Input(InputEvent::List) => { + return Ok(WatchExit::List); + } WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { watch_state.render()?; } @@ -147,5 +166,5 @@ We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. ")?; - Ok(()) + Ok(WatchExit::Shutdown) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 4db9440b..393ea02c 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -36,10 +36,11 @@ impl<'a> WatchState<'a> { let writer = io::stdout().lock(); let prompt = format!( - "\n\n{}int/{}lear/{}uit? ", + "\n\n{}int/{}lear/{}ist/{}uit? ", "h".bold(), "c".bold(), - "q".bold() + "l".bold(), + "q".bold(), ) .into_bytes(); From c9a5fa6097997e95bc415cd76ef931a1a4bb1510 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 02:19:14 +0200 Subject: [PATCH 094/433] Accept repeat keyboard events --- src/list.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/list.rs b/src/list.rs index c92b3692..560b85a8 100644 --- a/src/list.rs +++ b/src/list.rs @@ -28,13 +28,10 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul let key = loop { match event::read()? { - Event::Key(key) => { - if key.kind != KeyEventKind::Press { - continue; - } - - break key; - } + Event::Key(key) => match key.kind { + KeyEventKind::Press | KeyEventKind::Repeat => break key, + KeyEventKind::Release => (), + }, // Redraw Event::Resize(_, _) => continue 'outer, // Ignore From f034899c7f8de93ff572722b1cdf44f73c6452b5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 03:54:48 +0200 Subject: [PATCH 095/433] Capture terminal resize events --- src/watch.rs | 91 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 004a13f6..7b4a02dd 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,11 +1,12 @@ -use anyhow::Result; +use anyhow::{Error, Result}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, DebounceEventResult, DebouncedEventKind, }; use std::{ - io::{self, BufRead, Write}, + io::{self, Write}, path::Path, sync::mpsc::{channel, Sender}, thread, @@ -39,7 +40,7 @@ enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, NotifyErr(notify::Error), - StdinErr(io::Error), + TerminalEventErr(io::Error), TerminalResize, } @@ -81,37 +82,61 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { } } -fn input_handler(tx: Sender) { - let mut stdin = io::stdin().lock(); - let mut stdin_buf = String::with_capacity(8); +fn terminal_event_handler(tx: Sender) { + let mut input = String::with_capacity(8); loop { - if let Err(e) = stdin.read_line(&mut stdin_buf) { - // If `send` returns an error, then the receiver is dropped and - // a shutdown has been already initialized. - let _ = tx.send(WatchEvent::StdinErr(e)); - return; - } - - let event = match stdin_buf.trim() { - "h" | "hint" => InputEvent::Hint, - "c" | "clear" => InputEvent::Clear, - "l" | "list" => InputEvent::List, - "q" | "quit" => InputEvent::Quit, - _ => InputEvent::Unrecognized, + let terminal_event = match event::read() { + Ok(v) => v, + Err(e) => { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::TerminalEventErr(e)); + return; + } }; - if tx.send(WatchEvent::Input(event)).is_err() { - // The receiver was dropped. - return; - } + match terminal_event { + Event::Key(key) => { + match key.kind { + KeyEventKind::Release => continue, + KeyEventKind::Press | KeyEventKind::Repeat => (), + } - match event { - InputEvent::List | InputEvent::Quit => return, - _ => (), - } + match key.code { + KeyCode::Enter => { + let input_event = match input.trim() { + "h" | "hint" => InputEvent::Hint, + "c" | "clear" => InputEvent::Clear, + "l" | "list" => InputEvent::List, + "q" | "quit" => InputEvent::Quit, + _ => InputEvent::Unrecognized, + }; - stdin_buf.clear(); + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; + } + + match input_event { + InputEvent::List | InputEvent::Quit => return, + _ => (), + } + + input.clear(); + } + KeyCode::Char(c) => { + input.push(c); + } + _ => (), + } + } + Event::Resize(_, _) => { + if tx.send(WatchEvent::TerminalResize).is_err() { + return; + } + } + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, + } } } @@ -134,7 +159,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu watch_state.run_exercise()?; watch_state.render()?; - thread::spawn(move || input_handler(tx)); + thread::spawn(move || terminal_event_handler(tx)); while let Ok(event) = rx.recv() { match event { @@ -156,8 +181,12 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu watch_state.run_exercise_with_ind(exercise_ind)?; watch_state.render()?; } - WatchEvent::NotifyErr(e) => return Err(e.into()), - WatchEvent::StdinErr(e) => return Err(e.into()), + WatchEvent::NotifyErr(e) => { + return Err(Error::from(e).context("Exercise file watcher failed")) + } + WatchEvent::TerminalEventErr(e) => { + return Err(Error::from(e).context("Terminal event listener failed")) + } } } From a46d66134b26095e553f284c02de9a895e15f180 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 03:56:41 +0200 Subject: [PATCH 096/433] Fix shift of first output line --- src/watch/state.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/watch/state.rs b/src/watch/state.rs index 393ea02c..08707a45 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -127,6 +127,9 @@ You can keep working on this exercise or jump into the next one by removing the } pub fn render(&mut self) -> Result<()> { + // Prevent having the first line shifted after clearing because of the prompt. + self.writer.write_all(b"\n")?; + self.writer.execute(Clear(ClearType::All))?; if let Some(stdout) = &self.stdout { From 6255efe8b2de9d8d7f69871584444ab34fae122d Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 04:08:40 +0200 Subject: [PATCH 097/433] Show the invalid command to avoid confusion after resizing the terminal --- src/watch.rs | 24 ++++++++++-------------- src/watch/state.rs | 9 +++++++-- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 7b4a02dd..8b211035 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -27,13 +27,12 @@ pub enum WatchExit { List, } -#[derive(Copy, Clone)] enum InputEvent { Hint, Clear, List, Quit, - Unrecognized, + Unrecognized(String), } enum WatchEvent { @@ -85,7 +84,7 @@ impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { fn terminal_event_handler(tx: Sender) { let mut input = String::with_capacity(8); - loop { + let last_input_event = loop { let terminal_event = match event::read() { Ok(v) => v, Err(e) => { @@ -108,20 +107,15 @@ fn terminal_event_handler(tx: Sender) { let input_event = match input.trim() { "h" | "hint" => InputEvent::Hint, "c" | "clear" => InputEvent::Clear, - "l" | "list" => InputEvent::List, - "q" | "quit" => InputEvent::Quit, - _ => InputEvent::Unrecognized, + "l" | "list" => break InputEvent::List, + "q" | "quit" => break InputEvent::Quit, + _ => InputEvent::Unrecognized(input.clone()), }; if tx.send(WatchEvent::Input(input_event)).is_err() { return; } - match input_event { - InputEvent::List | InputEvent::Quit => return, - _ => (), - } - input.clear(); } KeyCode::Char(c) => { @@ -137,7 +131,9 @@ fn terminal_event_handler(tx: Sender) { } Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, } - } + }; + + let _ = tx.send(WatchEvent::Input(last_input_event)); } pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { @@ -173,8 +169,8 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu watch_state.render()?; } WatchEvent::Input(InputEvent::Quit) => break, - WatchEvent::Input(InputEvent::Unrecognized) => { - watch_state.handle_invalid_cmd()?; + WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { + watch_state.handle_invalid_cmd(&cmd)?; } WatchEvent::FileChange { exercise_ind } => { // TODO: bool diff --git a/src/watch/state.rs b/src/watch/state.rs index 08707a45..751285fc 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -159,8 +159,13 @@ You can keep working on this exercise or jump into the next one by removing the self.show_prompt() } - pub fn handle_invalid_cmd(&mut self) -> io::Result<()> { - self.writer.write_all(b"Invalid command")?; + pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { + self.writer.write_all(b"Invalid command: ")?; + self.writer.write_all(cmd.as_bytes())?; + if cmd.len() > 1 { + self.writer + .write_all(b" (confusing input can occur after resizing the terminal)")?; + } self.show_prompt() } } From 62e92476e6dad1fc191fd666eae2fccb263f5ff0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 04:10:05 +0200 Subject: [PATCH 098/433] Fix typo --- src/watch.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 8b211035..cf63627d 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -43,12 +43,12 @@ enum WatchEvent { TerminalResize, } -struct DebouceEventHandler { +struct DebounceEventHandler { tx: Sender, exercises: &'static [Exercise], } -impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler { +impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { fn handle_event(&mut self, event: DebounceEventResult) { let event = match event { Ok(event) => { @@ -140,7 +140,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), - DebouceEventHandler { + DebounceEventHandler { tx: tx.clone(), exercises, }, From a59acf88354c8dfba301e59173653bc9a5f4bfb2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 14:29:31 +0200 Subject: [PATCH 099/433] Show the current exercise path --- src/progress_bar.rs | 2 +- src/watch/state.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/progress_bar.rs b/src/progress_bar.rs index 97c8ad90..d6962b8c 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -49,7 +49,7 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result3}/{total} exercises").unwrap(); + writeln!(line, "] {progress:>3}/{total} exercises").unwrap(); Ok(line) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 751285fc..da5ac3d7 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -149,6 +149,12 @@ You can keep working on this exercise or jump into the next one by removing the let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; self.writer.write_all(progress_bar.as_bytes())?; + self.writer.write_all(b"Current exercise: ")?; + self.writer.write_fmt(format_args!( + "{}", + self.exercise.path.to_string_lossy().bold() + ))?; + self.show_prompt()?; Ok(()) From 193e0a03b2cde094b2a668371b7ed94f81d33de7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 14:31:08 +0200 Subject: [PATCH 100/433] Use light blue for the message --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 89189794..209374b1 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -186,7 +186,7 @@ impl UiState { "↓/j ↑/k home/g end/G │ filter one/

ending │ eset │ ontinue at │ uit", ) } else { - self.message.as_str().blue() + self.message.as_str().light_blue() }; frame.render_widget( message, From b3642b0219252e97213fd4348379f272a3002f39 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 14:35:42 +0200 Subject: [PATCH 101/433] Remove todo --- src/state_file.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/state_file.rs b/src/state_file.rs index 693c78dc..583e0436 100644 --- a/src/state_file.rs +++ b/src/state_file.rs @@ -33,7 +33,6 @@ impl StateFile { } fn write(&self) -> Result<()> { - // TODO: Capacity let mut buf = Vec::with_capacity(1024); serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; fs::write(".rustlings-state.json", buf) From 27e95206658e8f86cad351ce163f03c0d36e05ea Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 14:40:49 +0200 Subject: [PATCH 102/433] Add deny_unknown_fields --- src/exercise.rs | 2 ++ src/state_file.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/exercise.rs b/src/exercise.rs index 232d7f95..ca47009d 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -46,6 +46,7 @@ pub enum Mode { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct InfoFile { pub exercises: Vec, } @@ -65,6 +66,7 @@ impl InfoFile { // Deserialized from the `info.toml` file. #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct Exercise { // Name of the exercise pub name: String, diff --git a/src/state_file.rs b/src/state_file.rs index 583e0436..6b80354e 100644 --- a/src/state_file.rs +++ b/src/state_file.rs @@ -5,6 +5,7 @@ use std::fs; use crate::exercise::Exercise; #[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct StateFile { next_exercise_ind: usize, progress: Vec, From 256c4013b759368b97f08aeb38d1b03f2eb42d7a Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 15:56:38 +0200 Subject: [PATCH 103/433] Keep hint displayed after resizing the terminal --- src/watch.rs | 4 +--- src/watch/state.rs | 41 +++++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index cf63627d..6d791f4b 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -29,7 +29,6 @@ pub enum WatchExit { enum InputEvent { Hint, - Clear, List, Quit, Unrecognized(String), @@ -106,7 +105,6 @@ fn terminal_event_handler(tx: Sender) { KeyCode::Enter => { let input_event = match input.trim() { "h" | "hint" => InputEvent::Hint, - "c" | "clear" => InputEvent::Clear, "l" | "list" => break InputEvent::List, "q" | "quit" => break InputEvent::Quit, _ => InputEvent::Unrecognized(input.clone()), @@ -165,7 +163,7 @@ pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resu WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } - WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => { + WatchEvent::TerminalResize => { watch_state.render()?; } WatchEvent::Input(InputEvent::Quit) => break, diff --git a/src/watch/state.rs b/src/watch/state.rs index da5ac3d7..6f6d2f10 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use std::{ fmt::Write as _, - io::{self, StdoutLock, Write as _}, + io::{self, StdoutLock, Write}, }; use crate::{ @@ -24,7 +24,7 @@ pub struct WatchState<'a> { stdout: Option>, stderr: Option>, message: Option, - prompt: Vec, + hint_displayed: bool, } impl<'a> WatchState<'a> { @@ -35,15 +35,6 @@ impl<'a> WatchState<'a> { let writer = io::stdout().lock(); - let prompt = format!( - "\n\n{}int/{}lear/{}ist/{}uit? ", - "h".bold(), - "c".bold(), - "l".bold(), - "q".bold(), - ) - .into_bytes(); - Self { writer, exercises, @@ -53,7 +44,7 @@ impl<'a> WatchState<'a> { stdout: None, stderr: None, message: None, - prompt, + hint_displayed: false, } } @@ -122,7 +113,15 @@ You can keep working on this exercise or jump into the next one by removing the } pub fn show_prompt(&mut self) -> io::Result<()> { - self.writer.write_all(&self.prompt)?; + self.writer.write_all(b"\n\n")?; + + if !self.hint_displayed { + self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; + } + + self.writer + .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?; + self.writer.flush() } @@ -134,10 +133,12 @@ You can keep working on this exercise or jump into the next one by removing the if let Some(stdout) = &self.stdout { self.writer.write_all(stdout)?; + self.writer.write_all(b"\n")?; } if let Some(stderr) = &self.stderr { self.writer.write_all(stderr)?; + self.writer.write_all(b"\n")?; } if let Some(message) = &self.message { @@ -145,6 +146,14 @@ You can keep working on this exercise or jump into the next one by removing the } self.writer.write_all(b"\n")?; + + if self.hint_displayed { + self.writer + .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?; + self.writer.write_all(self.exercise.hint.as_bytes())?; + self.writer.write_all(b"\n\n")?; + } + let line_width = size()?.0; let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; self.writer.write_all(progress_bar.as_bytes())?; @@ -160,9 +169,9 @@ You can keep working on this exercise or jump into the next one by removing the Ok(()) } - pub fn show_hint(&mut self) -> io::Result<()> { - self.writer.write_all(self.exercise.hint.as_bytes())?; - self.show_prompt() + pub fn show_hint(&mut self) -> Result<()> { + self.hint_displayed = true; + self.render() } pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { From 4bb6bda9f6416e30233342e73fc9a8486faa3f98 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 10 Apr 2024 16:02:12 +0200 Subject: [PATCH 104/433] Separate event handlers --- src/watch.rs | 123 ++++-------------------------------- src/watch/debounce_event.rs | 44 +++++++++++++ src/watch/terminal_event.rs | 65 +++++++++++++++++++ 3 files changed, 123 insertions(+), 109 deletions(-) create mode 100644 src/watch/debounce_event.rs create mode 100644 src/watch/terminal_event.rs diff --git a/src/watch.rs b/src/watch.rs index 6d791f4b..b29169b3 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,38 +1,27 @@ use anyhow::{Error, Result}; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use notify_debouncer_mini::{ new_debouncer, notify::{self, RecursiveMode}, - DebounceEventResult, DebouncedEventKind, }; use std::{ io::{self, Write}, path::Path, - sync::mpsc::{channel, Sender}, + sync::mpsc::channel, thread, time::Duration, }; +mod debounce_event; mod state; +mod terminal_event; use crate::{exercise::Exercise, state_file::StateFile}; -use self::state::WatchState; - -/// Returned by the watch mode to indicate what to do afterwards. -pub enum WatchExit { - /// Exit the program. - Shutdown, - /// Enter the list mode and restart the watch mode afterwards. - List, -} - -enum InputEvent { - Hint, - List, - Quit, - Unrecognized(String), -} +use self::{ + debounce_event::DebounceEventHandler, + state::WatchState, + terminal_event::{terminal_event_handler, InputEvent}, +}; enum WatchEvent { Input(InputEvent), @@ -42,96 +31,12 @@ enum WatchEvent { TerminalResize, } -struct DebounceEventHandler { - tx: Sender, - exercises: &'static [Exercise], -} - -impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { - fn handle_event(&mut self, event: DebounceEventResult) { - let event = match event { - Ok(event) => { - let Some(exercise_ind) = event - .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { - return None; - } - - self.exercises - .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) - }) - .min() - else { - return; - }; - - WatchEvent::FileChange { exercise_ind } - } - Err(e) => WatchEvent::NotifyErr(e), - }; - - // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(event); - } -} - -fn terminal_event_handler(tx: Sender) { - let mut input = String::with_capacity(8); - - let last_input_event = loop { - let terminal_event = match event::read() { - Ok(v) => v, - Err(e) => { - // If `send` returns an error, then the receiver is dropped and - // a shutdown has been already initialized. - let _ = tx.send(WatchEvent::TerminalEventErr(e)); - return; - } - }; - - match terminal_event { - Event::Key(key) => { - match key.kind { - KeyEventKind::Release => continue, - KeyEventKind::Press | KeyEventKind::Repeat => (), - } - - match key.code { - KeyCode::Enter => { - let input_event = match input.trim() { - "h" | "hint" => InputEvent::Hint, - "l" | "list" => break InputEvent::List, - "q" | "quit" => break InputEvent::Quit, - _ => InputEvent::Unrecognized(input.clone()), - }; - - if tx.send(WatchEvent::Input(input_event)).is_err() { - return; - } - - input.clear(); - } - KeyCode::Char(c) => { - input.push(c); - } - _ => (), - } - } - Event::Resize(_, _) => { - if tx.send(WatchEvent::TerminalResize).is_err() { - return; - } - } - Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, - } - }; - - let _ = tx.send(WatchEvent::Input(last_input_event)); +/// Returned by the watch mode to indicate what to do afterwards. +pub enum WatchExit { + /// Exit the program. + Shutdown, + /// Enter the list mode and restart the watch mode afterwards. + List, } pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { diff --git a/src/watch/debounce_event.rs b/src/watch/debounce_event.rs new file mode 100644 index 00000000..1dc92cb4 --- /dev/null +++ b/src/watch/debounce_event.rs @@ -0,0 +1,44 @@ +use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; +use std::sync::mpsc::Sender; + +use crate::exercise::Exercise; + +use super::WatchEvent; + +pub struct DebounceEventHandler { + pub tx: Sender, + pub exercises: &'static [Exercise], +} + +impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { + fn handle_event(&mut self, event: DebounceEventResult) { + let event = match event { + Ok(event) => { + let Some(exercise_ind) = event + .iter() + .filter_map(|event| { + if event.kind != DebouncedEventKind::Any + || !event.path.extension().is_some_and(|ext| ext == "rs") + { + return None; + } + + self.exercises + .iter() + .position(|exercise| event.path.ends_with(&exercise.path)) + }) + .min() + else { + return; + }; + + WatchEvent::FileChange { exercise_ind } + } + Err(e) => WatchEvent::NotifyErr(e), + }; + + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.tx.send(event); + } +} diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs new file mode 100644 index 00000000..7c85b5bd --- /dev/null +++ b/src/watch/terminal_event.rs @@ -0,0 +1,65 @@ +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use std::sync::mpsc::Sender; + +use super::WatchEvent; + +pub enum InputEvent { + Hint, + List, + Quit, + Unrecognized(String), +} + +pub fn terminal_event_handler(tx: Sender) { + let mut input = String::with_capacity(8); + + let last_input_event = loop { + let terminal_event = match event::read() { + Ok(v) => v, + Err(e) => { + // If `send` returns an error, then the receiver is dropped and + // a shutdown has been already initialized. + let _ = tx.send(WatchEvent::TerminalEventErr(e)); + return; + } + }; + + match terminal_event { + Event::Key(key) => { + match key.kind { + KeyEventKind::Release => continue, + KeyEventKind::Press | KeyEventKind::Repeat => (), + } + + match key.code { + KeyCode::Enter => { + let input_event = match input.trim() { + "h" | "hint" => InputEvent::Hint, + "l" | "list" => break InputEvent::List, + "q" | "quit" => break InputEvent::Quit, + _ => InputEvent::Unrecognized(input.clone()), + }; + + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; + } + + input.clear(); + } + KeyCode::Char(c) => { + input.push(c); + } + _ => (), + } + } + Event::Resize(_, _) => { + if tx.send(WatchEvent::TerminalResize).is_err() { + return; + } + } + Event::FocusGained | Event::FocusLost | Event::Mouse(_) | Event::Paste(_) => continue, + } + }; + + let _ = tx.send(WatchEvent::Input(last_input_event)); +} From fa1f239a702eb2c0b7e0115e986481156961bbc8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 02:51:02 +0200 Subject: [PATCH 105/433] Remove "I AM NOT DONE" and the verify mode and add AppState --- Cargo.lock | 1 - Cargo.toml | 1 - README.md | 8 +- exercises/00_intro/intro1.rs | 4 +- exercises/00_intro/intro2.rs | 2 - exercises/01_variables/variables1.rs | 2 - exercises/01_variables/variables2.rs | 2 - exercises/01_variables/variables3.rs | 2 - exercises/01_variables/variables4.rs | 2 - exercises/01_variables/variables5.rs | 2 - exercises/01_variables/variables6.rs | 2 - exercises/02_functions/functions1.rs | 2 - exercises/02_functions/functions2.rs | 2 - exercises/02_functions/functions3.rs | 2 - exercises/02_functions/functions4.rs | 2 - exercises/02_functions/functions5.rs | 2 - exercises/03_if/if1.rs | 2 - exercises/03_if/if2.rs | 2 - exercises/03_if/if3.rs | 2 - .../04_primitive_types/primitive_types1.rs | 2 - .../04_primitive_types/primitive_types2.rs | 2 - .../04_primitive_types/primitive_types3.rs | 2 - .../04_primitive_types/primitive_types4.rs | 2 - .../04_primitive_types/primitive_types5.rs | 2 - .../04_primitive_types/primitive_types6.rs | 2 - exercises/05_vecs/vecs1.rs | 2 - exercises/05_vecs/vecs2.rs | 2 - .../06_move_semantics/move_semantics1.rs | 2 - .../06_move_semantics/move_semantics2.rs | 2 - .../06_move_semantics/move_semantics3.rs | 2 - .../06_move_semantics/move_semantics4.rs | 2 - .../06_move_semantics/move_semantics5.rs | 2 - .../06_move_semantics/move_semantics6.rs | 2 - exercises/07_structs/structs1.rs | 2 - exercises/07_structs/structs2.rs | 2 - exercises/07_structs/structs3.rs | 2 - exercises/08_enums/enums1.rs | 2 - exercises/08_enums/enums2.rs | 2 - exercises/08_enums/enums3.rs | 2 - exercises/09_strings/strings1.rs | 2 - exercises/09_strings/strings2.rs | 2 - exercises/09_strings/strings3.rs | 2 - exercises/09_strings/strings4.rs | 2 - exercises/10_modules/modules1.rs | 2 - exercises/10_modules/modules2.rs | 2 - exercises/10_modules/modules3.rs | 2 - exercises/11_hashmaps/hashmaps1.rs | 2 - exercises/11_hashmaps/hashmaps2.rs | 2 - exercises/11_hashmaps/hashmaps3.rs | 2 - exercises/12_options/options1.rs | 2 - exercises/12_options/options2.rs | 2 - exercises/12_options/options3.rs | 2 - exercises/13_error_handling/errors1.rs | 2 - exercises/13_error_handling/errors2.rs | 2 - exercises/13_error_handling/errors3.rs | 2 - exercises/13_error_handling/errors4.rs | 2 - exercises/13_error_handling/errors5.rs | 2 - exercises/13_error_handling/errors6.rs | 2 - exercises/14_generics/generics1.rs | 2 - exercises/14_generics/generics2.rs | 2 - exercises/15_traits/traits1.rs | 2 - exercises/15_traits/traits2.rs | 2 - exercises/15_traits/traits3.rs | 2 - exercises/15_traits/traits4.rs | 2 - exercises/15_traits/traits5.rs | 2 - exercises/16_lifetimes/lifetimes1.rs | 2 - exercises/16_lifetimes/lifetimes2.rs | 2 - exercises/16_lifetimes/lifetimes3.rs | 2 - exercises/17_tests/tests1.rs | 2 - exercises/17_tests/tests2.rs | 2 - exercises/17_tests/tests3.rs | 2 - exercises/17_tests/tests4.rs | 2 - exercises/18_iterators/iterators1.rs | 2 - exercises/18_iterators/iterators2.rs | 2 - exercises/18_iterators/iterators3.rs | 2 - exercises/18_iterators/iterators4.rs | 2 - exercises/18_iterators/iterators5.rs | 2 - exercises/19_smart_pointers/arc1.rs | 2 - exercises/19_smart_pointers/box1.rs | 2 - exercises/19_smart_pointers/cow1.rs | 2 - exercises/19_smart_pointers/rc1.rs | 2 - exercises/20_threads/threads1.rs | 2 - exercises/20_threads/threads2.rs | 2 - exercises/20_threads/threads3.rs | 2 - exercises/21_macros/macros1.rs | 2 - exercises/21_macros/macros2.rs | 2 - exercises/21_macros/macros3.rs | 2 - exercises/21_macros/macros4.rs | 2 - exercises/22_clippy/clippy1.rs | 2 - exercises/22_clippy/clippy2.rs | 2 - exercises/22_clippy/clippy3.rs | 2 - exercises/23_conversions/as_ref_mut.rs | 2 - exercises/23_conversions/from_into.rs | 2 - exercises/23_conversions/from_str.rs | 2 - exercises/23_conversions/try_from_into.rs | 2 - exercises/23_conversions/using_as.rs | 2 - exercises/quiz1.rs | 2 - exercises/quiz2.rs | 2 - exercises/quiz3.rs | 2 - info.toml | 7 +- src/app_state.rs | 185 ++++++++++++++++ src/exercise.rs | 206 +----------------- src/list.rs | 21 +- src/list/state.rs | 71 +++--- src/main.rs | 67 ++---- src/run.rs | 23 +- src/state_file.rs | 68 ------ src/verify.rs | 85 -------- src/watch.rs | 10 +- src/watch/state.rs | 96 ++------ .../state/exercises/pending_exercise.rs | 2 - .../state/exercises/pending_test_exercise.rs | 2 - tests/integration_tests.rs | 28 +-- 113 files changed, 306 insertions(+), 769 deletions(-) create mode 100644 src/app_state.rs delete mode 100644 src/state_file.rs delete mode 100644 src/verify.rs diff --git a/Cargo.lock b/Cargo.lock index ee469437..aeb6c61f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,7 +699,6 @@ dependencies = [ "serde_json", "toml_edit", "which", - "winnow", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index da09ba18..435dfd49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ serde_json = "1.0.115" serde.workspace = true toml_edit.workspace = true which = "6.0.1" -winnow = "0.6.5" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/README.md b/README.md index 6b9c9833..fd76fdff 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,7 @@ The task is simple. Most exercises contain an error that keeps them from compili rustlings watch ``` -This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. If you want to only run it once, you can use: - -```bash -rustlings verify -``` - -This will do the same as watch, but it'll quit after running. +This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. In case you want to go by your own order, or want to only verify a single exercise, you can run: diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 5dd18b45..aa505a13 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,6 +1,6 @@ // intro1.rs // -// About this `I AM NOT DONE` thing: +// TODO: Update comment // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel // ready for the next exercise, remove the `I AM NOT DONE` comment below. @@ -13,8 +13,6 @@ // Execute `rustlings hint intro1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { println!("Hello and"); println!(r#" welcome to... "#); diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs index a28ad3dc..84e0d75c 100644 --- a/exercises/00_intro/intro2.rs +++ b/exercises/00_intro/intro2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint intro2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { printline!("Hello there!") } diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs index b3e089a5..56408f35 100644 --- a/exercises/01_variables/variables1.rs +++ b/exercises/01_variables/variables1.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint variables1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { x = 5; println!("x has the value {}", x); diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs index e1c23edf..0f417e01 100644 --- a/exercises/01_variables/variables2.rs +++ b/exercises/01_variables/variables2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let x; if x == 10 { diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs index 86bed419..421c6b15 100644 --- a/exercises/01_variables/variables3.rs +++ b/exercises/01_variables/variables3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let x: i32; println!("Number {}", x); diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs index 5394f394..68f8f50b 100644 --- a/exercises/01_variables/variables4.rs +++ b/exercises/01_variables/variables4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let x = 3; println!("Number {}", x); diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index a29b38be..7014c568 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let number = "T-H-R-E-E"; // don't change this line println!("Spell a Number : {}", number); diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs index 853183ba..9f476825 100644 --- a/exercises/01_variables/variables6.rs +++ b/exercises/01_variables/variables6.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint variables6` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - const NUMBER = 3; fn main() { println!("Number {}", NUMBER); diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs index 40ed9a07..2365f91b 100644 --- a/exercises/02_functions/functions1.rs +++ b/exercises/02_functions/functions1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { call_me(); } diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs index 5154f34d..64dbd665 100644 --- a/exercises/02_functions/functions2.rs +++ b/exercises/02_functions/functions2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { call_me(3); } diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs index 74f44d6d..50371212 100644 --- a/exercises/02_functions/functions3.rs +++ b/exercises/02_functions/functions3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { call_me(); } diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs index 77c4b2aa..6b449edf 100644 --- a/exercises/02_functions/functions4.rs +++ b/exercises/02_functions/functions4.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint functions4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let original_price = 51; println!("Your sale price is {}", sale_price(original_price)); diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs index f1b63f48..0c963223 100644 --- a/exercises/02_functions/functions5.rs +++ b/exercises/02_functions/functions5.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint functions5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let answer = square(3); println!("The square of 3 is {}", answer); diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs index d2afccf8..a1df66bb 100644 --- a/exercises/03_if/if1.rs +++ b/exercises/03_if/if1.rs @@ -2,8 +2,6 @@ // // Execute `rustlings hint if1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub fn bigger(a: i32, b: i32) -> i32 { // Complete this function to return the bigger number! // If both numbers are equal, any of them can be returned. diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index f512f13f..7b9c05f6 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -5,8 +5,6 @@ // // Execute `rustlings hint if2` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { "foo" diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index 16962740..caba172b 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -2,8 +2,6 @@ // // Execute `rustlings hint if3` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub fn animal_habitat(animal: &str) -> &'static str { let identifier = if animal == "crab" { 1 diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs index 36633400..f9169c84 100644 --- a/exercises/04_primitive_types/primitive_types1.rs +++ b/exercises/04_primitive_types/primitive_types1.rs @@ -3,8 +3,6 @@ // Fill in the rest of the line that has code missing! No hints, there's no // tricks, just get used to typing these :) -// I AM NOT DONE - fn main() { // Booleans (`bool`) diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs index f1616ed3..1911b12a 100644 --- a/exercises/04_primitive_types/primitive_types2.rs +++ b/exercises/04_primitive_types/primitive_types2.rs @@ -3,8 +3,6 @@ // Fill in the rest of the line that has code missing! No hints, there's no // tricks, just get used to typing these :) -// I AM NOT DONE - fn main() { // Characters (`char`) diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs index 8b0de44e..70a8cc20 100644 --- a/exercises/04_primitive_types/primitive_types3.rs +++ b/exercises/04_primitive_types/primitive_types3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint primitive_types3` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - fn main() { let a = ??? diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index d44d8776..8ed0a82a 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn slice_out_of_array() { let a = [1, 2, 3, 4, 5]; diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs index f646986e..5754a3d8 100644 --- a/exercises/04_primitive_types/primitive_types5.rs +++ b/exercises/04_primitive_types/primitive_types5.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint primitive_types5` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - fn main() { let cat = ("Furry McFurson", 3.5); let /* your pattern here */ = cat; diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs index 07cc46c6..5f82f10f 100644 --- a/exercises/04_primitive_types/primitive_types6.rs +++ b/exercises/04_primitive_types/primitive_types6.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn indexing_tuple() { let numbers = (1, 2, 3); diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index 65b7a7f8..c64acbbd 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -7,8 +7,6 @@ // // Execute `rustlings hint vecs1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - fn array_and_vec() -> ([i32; 4], Vec) { let a = [10, 20, 30, 40]; // a plain array let v = // TODO: declare your vector here with the macro for vectors diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index e92c970a..d64d3d16 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -7,8 +7,6 @@ // // Execute `rustlings hint vecs2` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - fn vec_loop(mut v: Vec) -> Vec { for element in v.iter_mut() { // TODO: Fill this up so that each element in the Vec `v` is diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs index e0639375..c612ba93 100644 --- a/exercises/06_move_semantics/move_semantics1.rs +++ b/exercises/06_move_semantics/move_semantics1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs index dc58be50..3457d111 100644 --- a/exercises/06_move_semantics/move_semantics2.rs +++ b/exercises/06_move_semantics/move_semantics2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs index 7152c716..9415eb15 100644 --- a/exercises/06_move_semantics/move_semantics3.rs +++ b/exercises/06_move_semantics/move_semantics3.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs index bfc917fa..1509f5d2 100644 --- a/exercises/06_move_semantics/move_semantics4.rs +++ b/exercises/06_move_semantics/move_semantics4.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let vec0 = vec![22, 44, 66]; diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs index 267bdccc..c84d2fea 100644 --- a/exercises/06_move_semantics/move_semantics5.rs +++ b/exercises/06_move_semantics/move_semantics5.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - #[test] fn main() { let mut x = 100; diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs index cace4ca6..6059e61c 100644 --- a/exercises/06_move_semantics/move_semantics6.rs +++ b/exercises/06_move_semantics/move_semantics6.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand // for a hint. -// I AM NOT DONE - fn main() { let data = "Rust is great!".to_string(); diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs index 5fa5821c..29781214 100644 --- a/exercises/07_structs/structs1.rs +++ b/exercises/07_structs/structs1.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint structs1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct ColorClassicStruct { // TODO: Something goes here } diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs index 328567f0..a7a2deca 100644 --- a/exercises/07_structs/structs2.rs +++ b/exercises/07_structs/structs2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint structs2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug)] struct Order { name: String, diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 7cda5af1..9835b811 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint structs3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug)] struct Package { sender_country: String, diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs index 25525b25..330269ca 100644 --- a/exercises/08_enums/enums1.rs +++ b/exercises/08_enums/enums1.rs @@ -2,8 +2,6 @@ // // No hints this time! ;) -// I AM NOT DONE - #[derive(Debug)] enum Message { // TODO: define a few types of messages as used below diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs index df93fe0f..f0e4e6d3 100644 --- a/exercises/08_enums/enums2.rs +++ b/exercises/08_enums/enums2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint enums2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug)] enum Message { // TODO: define the different variants used below diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index 92d18c46..580a553e 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint enums3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - enum Message { // TODO: implement the message variant types based on their usage below } diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs index f50e1fa9..a1255a32 100644 --- a/exercises/09_strings/strings1.rs +++ b/exercises/09_strings/strings1.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint strings1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let answer = current_favorite_color(); println!("My current favorite color is {}", answer); diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs index 4d95d16a..ba76fe65 100644 --- a/exercises/09_strings/strings2.rs +++ b/exercises/09_strings/strings2.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint strings2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let word = String::from("green"); // Try not changing this line :) if is_a_color_word(word) { diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index 384e7ce3..dedc081f 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint strings3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn trim_me(input: &str) -> String { // TODO: Remove whitespace from both ends of a string! ??? diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index e8c54acc..a034aa49 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -7,8 +7,6 @@ // // No hints this time! -// I AM NOT DONE - fn string_slice(arg: &str) { println!("{}", arg); } diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs index 9eb5a48b..c750946c 100644 --- a/exercises/10_modules/modules1.rs +++ b/exercises/10_modules/modules1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint modules1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - mod sausage_factory { // Don't let anybody outside of this module see this! fn get_secret_recipe() -> String { diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 04154543..4d3106c4 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint modules2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - mod delicious_snacks { // TODO: Fix these use statements use self::fruits::PEAR as ??? diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs index f2bb0503..c211a769 100644 --- a/exercises/10_modules/modules3.rs +++ b/exercises/10_modules/modules3.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint modules3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // TODO: Complete this use statement use ??? diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs index 80829eaa..5a52f611 100644 --- a/exercises/11_hashmaps/hashmaps1.rs +++ b/exercises/11_hashmaps/hashmaps1.rs @@ -11,8 +11,6 @@ // Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; fn fruit_basket() -> HashMap { diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index a5925690..27306439 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -14,8 +14,6 @@ // Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; #[derive(Hash, PartialEq, Eq)] diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 8d9236df..775a4014 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -15,8 +15,6 @@ // Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; // A structure to store the goal details of a team. diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index 3cbfecd6..ba4b1cda 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint options1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // This function returns how much icecream there is left in the fridge. // If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it // all, so there'll be no more left :( diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs index 4d998e7d..73f707e3 100644 --- a/exercises/12_options/options2.rs +++ b/exercises/12_options/options2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint options2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[cfg(test)] mod tests { #[test] diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs index 23c15eab..7922ef92 100644 --- a/exercises/12_options/options3.rs +++ b/exercises/12_options/options3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint options3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Point { x: i32, y: i32, diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 0ba59a57..9767f2c8 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint errors1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub fn generate_nametag_text(name: String) -> Option { if name.is_empty() { // Empty names aren't allowed. diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index 631fe67f..88d1bf43 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -19,8 +19,6 @@ // Execute `rustlings hint errors2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::num::ParseIntError; pub fn total_cost(item_quantity: &str) -> Result { diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index d42d3b17..56bb31b1 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint errors3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::num::ParseIntError; fn main() { diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index d6d6fcb6..0e5c08bf 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint errors4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 92461a7e..0bcb4b8c 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -22,8 +22,6 @@ // Execute `rustlings hint errors5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::error; use std::fmt; use std::num::ParseIntError; diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index aaf0948e..de73a9a5 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint errors6` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::num::ParseIntError; // This is a custom error type that we will be using in `parse_pos_nonzero()`. diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs index 35c1d2fe..545fd95c 100644 --- a/exercises/14_generics/generics1.rs +++ b/exercises/14_generics/generics1.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint generics1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let mut shopping_list: Vec = Vec::new(); shopping_list.push("milk"); diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index 074cd938..d50ed174 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint generics2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Wrapper { value: u32, } diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs index 37dfcbfe..c51d3b88 100644 --- a/exercises/15_traits/traits1.rs +++ b/exercises/15_traits/traits1.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint traits1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - trait AppendBar { fn append_bar(self) -> Self; } diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index 3e35f8e1..9a2bc07a 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -8,8 +8,6 @@ // // Execute `rustlings hint traits2` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - trait AppendBar { fn append_bar(self) -> Self; } diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 4e2b06b0..357f1d7f 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint traits3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub trait Licensed { fn licensing_info(&self) -> String; } diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index 4bda3e57..7242c483 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint traits4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub trait Licensed { fn licensing_info(&self) -> String { "some information".to_string() diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs index df183805..f258d327 100644 --- a/exercises/15_traits/traits5.rs +++ b/exercises/15_traits/traits5.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint traits5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub trait SomeTrait { fn some_function(&self) -> bool { true diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs index 87bde490..4f544b41 100644 --- a/exercises/16_lifetimes/lifetimes1.rs +++ b/exercises/16_lifetimes/lifetimes1.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs index 4f3d8c18..33b5565f 100644 --- a/exercises/16_lifetimes/lifetimes2.rs +++ b/exercises/16_lifetimes/lifetimes2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs index 9c59f9c0..de6005ec 100644 --- a/exercises/16_lifetimes/lifetimes3.rs +++ b/exercises/16_lifetimes/lifetimes3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Book { author: &str, title: &str, diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs index 810277ac..bde21083 100644 --- a/exercises/17_tests/tests1.rs +++ b/exercises/17_tests/tests1.rs @@ -10,8 +10,6 @@ // Execute `rustlings hint tests1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[cfg(test)] mod tests { #[test] diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs index f8024e9f..aea5c0e4 100644 --- a/exercises/17_tests/tests2.rs +++ b/exercises/17_tests/tests2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint tests2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[cfg(test)] mod tests { #[test] diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index 4013e384..d815e058 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint tests3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub fn is_even(num: i32) -> bool { num % 2 == 0 } diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs index 935d0db1..0972a5b4 100644 --- a/exercises/17_tests/tests4.rs +++ b/exercises/17_tests/tests4.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint tests4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - struct Rectangle { width: i32, height: i32 diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index 31076bb9..7ec7da2c 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint iterators1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[test] fn main() { let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"]; diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index dda82a08..4ca7742e 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -6,8 +6,6 @@ // Execute `rustlings hint iterators2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // Step 1. // Complete the `capitalize_first` function. // "hello" -> "Hello" diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 29fa23a3..f7da049c 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint iterators3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[derive(Debug, PartialEq, Eq)] pub enum DivisionError { NotDivisible(NotDivisibleError), diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index 3c0724e9..af3958c8 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint iterators4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - pub fn factorial(num: u64) -> u64 { // Complete this function to return the factorial of num // Do not use: diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs index a062ee4c..ceec5369 100644 --- a/exercises/18_iterators/iterators5.rs +++ b/exercises/18_iterators/iterators5.rs @@ -11,8 +11,6 @@ // Execute `rustlings hint iterators5` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::collections::HashMap; #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs index 3526ddcb..0647eea7 100644 --- a/exercises/19_smart_pointers/arc1.rs +++ b/exercises/19_smart_pointers/arc1.rs @@ -21,8 +21,6 @@ // // Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - #![forbid(unused_imports)] // Do not change this, (or the next) line. use std::sync::Arc; use std::thread; diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index 513e7daa..2abc0249 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -18,8 +18,6 @@ // // Execute `rustlings hint box1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - #[derive(PartialEq, Debug)] pub enum List { Cons(i32, List), diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs index fcd3e0bb..b24591b7 100644 --- a/exercises/19_smart_pointers/cow1.rs +++ b/exercises/19_smart_pointers/cow1.rs @@ -12,8 +12,6 @@ // // Execute `rustlings hint cow1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - use std::borrow::Cow; fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> { diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs index 1b903469..e96e6255 100644 --- a/exercises/19_smart_pointers/rc1.rs +++ b/exercises/19_smart_pointers/rc1.rs @@ -10,8 +10,6 @@ // // Execute `rustlings hint rc1` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - use std::rc::Rc; #[derive(Debug)] diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs index 80b6def3..be1301d7 100644 --- a/exercises/20_threads/threads1.rs +++ b/exercises/20_threads/threads1.rs @@ -8,8 +8,6 @@ // Execute `rustlings hint threads1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::thread; use std::time::{Duration, Instant}; diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs index 60d68241..13cb840e 100644 --- a/exercises/20_threads/threads2.rs +++ b/exercises/20_threads/threads2.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint threads2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::sync::Arc; use std::thread; use std::time::Duration; diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index acb97b4b..35b914ac 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint threads3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::sync::mpsc; use std::sync::Arc; use std::thread; diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs index 678de6ee..65986db0 100644 --- a/exercises/21_macros/macros1.rs +++ b/exercises/21_macros/macros1.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint macros1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - macro_rules! my_macro { () => { println!("Check out my macro!"); diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs index 788fc16a..b7c37fd9 100644 --- a/exercises/21_macros/macros2.rs +++ b/exercises/21_macros/macros2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint macros2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { my_macro!(); } diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs index b795c149..92a19227 100644 --- a/exercises/21_macros/macros3.rs +++ b/exercises/21_macros/macros3.rs @@ -5,8 +5,6 @@ // Execute `rustlings hint macros3` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - mod macros { macro_rules! my_macro { () => { diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs index 71b45a09..83a6e44f 100644 --- a/exercises/21_macros/macros4.rs +++ b/exercises/21_macros/macros4.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint macros4` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - #[rustfmt::skip] macro_rules! my_macro { () => { diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs index e0c6ce7c4..1e0f42e2 100644 --- a/exercises/22_clippy/clippy1.rs +++ b/exercises/22_clippy/clippy1.rs @@ -9,8 +9,6 @@ // Execute `rustlings hint clippy1` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - use std::f32; fn main() { diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs index 9b87a0b7..37ac089e 100644 --- a/exercises/22_clippy/clippy2.rs +++ b/exercises/22_clippy/clippy2.rs @@ -3,8 +3,6 @@ // Execute `rustlings hint clippy2` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn main() { let mut res = 42; let option = Some(12); diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index 5a95f5b8..6a6a36b5 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -3,8 +3,6 @@ // Here's a couple more easy Clippy fixes, so you can see its utility. // No hints. -// I AM NOT DONE - #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<()> = None; diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index 2ba9e3f0..cd2c93be 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -7,8 +7,6 @@ // Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - // Obtain the number of bytes (not characters) in the given argument. // TODO: Add the AsRef trait appropriately as a trait bound. fn byte_counter(arg: T) -> usize { diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs index 11787c37..d2a1609e 100644 --- a/exercises/23_conversions/from_into.rs +++ b/exercises/23_conversions/from_into.rs @@ -41,8 +41,6 @@ impl Default for Person { // If while parsing the age, something goes wrong, then return the default of // Person Otherwise, then return an instantiated Person object with the results -// I AM NOT DONE - impl From<&str> for Person { fn from(s: &str) -> Person {} } diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index e2093474..ed91ca5c 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -31,8 +31,6 @@ enum ParsePersonError { ParseInt(ParseIntError), } -// I AM NOT DONE - // Steps: // 1. If the length of the provided string is 0, an error should be returned // 2. Split the given string on the commas present in it diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs index 32d6ef39..23166555 100644 --- a/exercises/23_conversions/try_from_into.rs +++ b/exercises/23_conversions/try_from_into.rs @@ -27,8 +27,6 @@ enum IntoColorError { IntConversion, } -// I AM NOT DONE - // Your task is to complete this implementation and return an Ok result of inner // type Color. You need to create an implementation for a tuple of three // integers, an array of three integers, and a slice of integers. diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs index 414cef3a..9f617ec5 100644 --- a/exercises/23_conversions/using_as.rs +++ b/exercises/23_conversions/using_as.rs @@ -10,8 +10,6 @@ // Execute `rustlings hint using_as` or use the `hint` watch subcommand for a // hint. -// I AM NOT DONE - fn average(values: &[f64]) -> f64 { let total = values.iter().sum::(); total / values.len() diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs index 4ee5ada7..b9e71f59 100644 --- a/exercises/quiz1.rs +++ b/exercises/quiz1.rs @@ -13,8 +13,6 @@ // // No hints this time ;) -// I AM NOT DONE - // Put your function here! // fn calculate_price_of_apples { diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs index 29925caf..8ace3fe0 100644 --- a/exercises/quiz2.rs +++ b/exercises/quiz2.rs @@ -20,8 +20,6 @@ // // No hints this time! -// I AM NOT DONE - pub enum Command { Uppercase, Trim, diff --git a/exercises/quiz3.rs b/exercises/quiz3.rs index 3b01d313..24f70829 100644 --- a/exercises/quiz3.rs +++ b/exercises/quiz3.rs @@ -16,8 +16,6 @@ // // Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint. -// I AM NOT DONE - pub struct ReportCard { pub grade: f32, pub student_name: String, diff --git a/info.toml b/info.toml index 36629b38..c085e89c 100644 --- a/info.toml +++ b/info.toml @@ -4,6 +4,7 @@ name = "intro1" path = "exercises/00_intro/intro1.rs" mode = "compile" +# TODO: Fix hint hint = """ Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file to move on to the next exercise.""" @@ -129,11 +130,7 @@ path = "exercises/02_functions/functions3.rs" mode = "compile" hint = """ This time, the function *declaration* is okay, but there's something wrong -with the place where we're calling the function. - -As a reminder, you can freely play around with different solutions in Rustlings! -Watch mode will only jump to the next exercise if you remove the `I AM NOT -DONE` comment.""" +with the place where we're calling the function.""" [[exercises]] name = "functions4" diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 00000000..4a0912e4 --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,185 @@ +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; + +use crate::exercise::Exercise; + +const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct StateFile { + current_exercise_ind: usize, + progress: Vec, +} + +impl StateFile { + fn read(exercises: &[Exercise]) -> Option { + let file_content = fs::read(".rustlings-state.json").ok()?; + + let slf: Self = serde_json::de::from_slice(&file_content).ok()?; + + if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() { + return None; + } + + Some(slf) + } + + fn read_or_default(exercises: &[Exercise]) -> Self { + Self::read(exercises).unwrap_or_else(|| Self { + current_exercise_ind: 0, + progress: vec![false; exercises.len()], + }) + } + + fn write(&self) -> Result<()> { + let mut buf = Vec::with_capacity(1024); + serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; + fs::write(".rustlings-state.json", buf) + .context("Failed to write the state file `.rustlings-state.json`")?; + + Ok(()) + } +} + +pub struct AppState { + state_file: StateFile, + exercises: &'static [Exercise], + n_done: u16, + current_exercise: &'static Exercise, +} + +#[must_use] +pub enum ExercisesProgress { + AllDone, + Pending, +} + +impl AppState { + pub fn new(exercises: Vec) -> Self { + // Leaking for sending the exercises to the debounce event handler. + // Leaking is not a problem since the exercises' slice is used until the end of the program. + let exercises = exercises.leak(); + + let state_file = StateFile::read_or_default(exercises); + let n_done = state_file + .progress + .iter() + .fold(0, |acc, done| acc + u16::from(*done)); + let current_exercise = &exercises[state_file.current_exercise_ind]; + + Self { + state_file, + exercises, + n_done, + current_exercise, + } + } + + #[inline] + pub fn current_exercise_ind(&self) -> usize { + self.state_file.current_exercise_ind + } + + #[inline] + pub fn progress(&self) -> &[bool] { + &self.state_file.progress + } + + #[inline] + pub fn exercises(&self) -> &'static [Exercise] { + self.exercises + } + + #[inline] + pub fn n_done(&self) -> u16 { + self.n_done + } + + #[inline] + pub fn current_exercise(&self) -> &'static Exercise { + self.current_exercise + } + + pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { + if ind >= self.exercises.len() { + bail!(BAD_INDEX_ERR); + } + + self.state_file.current_exercise_ind = ind; + self.current_exercise = &self.exercises[ind]; + + self.state_file.write() + } + + pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { + let (ind, exercise) = self + .exercises + .iter() + .enumerate() + .find(|(_, exercise)| exercise.name == name) + .with_context(|| format!("No exercise found for '{name}'!"))?; + + self.state_file.current_exercise_ind = ind; + self.current_exercise = exercise; + + self.state_file.write() + } + + pub fn set_pending(&mut self, ind: usize) -> Result<()> { + let done = self + .state_file + .progress + .get_mut(ind) + .context(BAD_INDEX_ERR)?; + + if *done { + *done = false; + self.n_done -= 1; + self.state_file.write()?; + } + + Ok(()) + } + + fn next_exercise_ind(&self) -> Option { + let current_ind = self.state_file.current_exercise_ind; + + if current_ind == self.state_file.progress.len() - 1 { + // The last exercise is done. + // Search for exercises not done from the start. + return self.state_file.progress[..current_ind] + .iter() + .position(|done| !done); + } + + // The done exercise isn't the last one. + // Search for a pending exercise after the current one and then from the start. + match self.state_file.progress[current_ind + 1..] + .iter() + .position(|done| !done) + { + Some(ind) => Some(current_ind + 1 + ind), + None => self.state_file.progress[..current_ind] + .iter() + .position(|done| !done), + } + } + + pub fn done_current_exercise(&mut self) -> Result { + let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; + if !*done { + *done = true; + self.n_done += 1; + } + + let Some(ind) = self.next_exercise_ind() else { + return Ok(ExercisesProgress::AllDone); + }; + + self.set_current_exercise_ind(ind)?; + + Ok(ExercisesProgress::Pending) + } +} diff --git a/src/exercise.rs b/src/exercise.rs index ca47009d..de435d13 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,38 +1,14 @@ use anyhow::{Context, Result}; use serde::Deserialize; use std::{ - array, fmt::{self, Debug, Display, Formatter}, - fs::{self, File}, - io::{self, BufRead, BufReader}, - mem, + fs::{self}, path::PathBuf, process::{Command, Output}, }; -use winnow::{ - ascii::{space0, Caseless}, - combinator::opt, - Parser, -}; use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; -// The number of context lines above and below a highlighted line. -const CONTEXT: usize = 2; - -// Check if the line contains the "I AM NOT DONE" comment. -fn contains_not_done_comment(input: &str) -> bool { - ( - space0::<_, ()>, - "//", - opt('/'), - space0, - Caseless("I AM NOT DONE"), - ) - .parse_next(&mut &*input) - .is_ok() -} - // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] @@ -78,13 +54,6 @@ pub struct Exercise { pub hint: String, } -// The state of an Exercise. -#[derive(PartialEq, Eq, Debug)] -pub enum State { - Done, - Pending(Vec), -} - // The context information of a pending exercise. #[derive(PartialEq, Eq, Debug)] pub struct ContextLine { @@ -129,105 +98,6 @@ impl Exercise { } } - pub fn state(&self) -> Result { - let source_file = File::open(&self.path) - .with_context(|| format!("Failed to open the exercise file {}", self.path.display()))?; - let mut source_reader = BufReader::new(source_file); - - // Read the next line into `buf` without the newline at the end. - let mut read_line = |buf: &mut String| -> io::Result<_> { - let n = source_reader.read_line(buf)?; - if buf.ends_with('\n') { - buf.pop(); - if buf.ends_with('\r') { - buf.pop(); - } - } - Ok(n) - }; - - let mut current_line_number: usize = 1; - // Keep the last `CONTEXT` lines while iterating over the file lines. - let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256)); - let mut line = String::with_capacity(256); - - loop { - let n = read_line(&mut line).with_context(|| { - format!("Failed to read the exercise file {}", self.path.display()) - })?; - - // Reached the end of the file and didn't find the comment. - if n == 0 { - return Ok(State::Done); - } - - if contains_not_done_comment(&line) { - let mut context = Vec::with_capacity(2 * CONTEXT + 1); - // Previous lines. - for (ind, prev_line) in prev_lines - .into_iter() - .take(current_line_number - 1) - .enumerate() - .rev() - { - context.push(ContextLine { - line: prev_line, - number: current_line_number - 1 - ind, - important: false, - }); - } - - // Current line. - context.push(ContextLine { - line, - number: current_line_number, - important: true, - }); - - // Next lines. - for ind in 0..CONTEXT { - let mut next_line = String::with_capacity(256); - let Ok(n) = read_line(&mut next_line) else { - // If an error occurs, just ignore the next lines. - break; - }; - - // Reached the end of the file. - if n == 0 { - break; - } - - context.push(ContextLine { - line: next_line, - number: current_line_number + 1 + ind, - important: false, - }); - } - - return Ok(State::Pending(context)); - } - - current_line_number += 1; - // Add the current line as a previous line and shift the older lines by one. - for prev_line in &mut prev_lines { - mem::swap(&mut line, prev_line); - } - // The current line now contains the oldest previous line. - // Recycle it for reading the next line. - line.clear(); - } - } - - // Check that the exercise looks to be solved using self.state() - // This is not the best way to check since - // the user can just remove the "I AM NOT DONE" string from the file - // without actually having solved anything. - // The only other way to truly check this would to compile and run - // the exercise; which would be both costly and counterintuitive - pub fn looks_done(&self) -> Result { - self.state().map(|state| state == State::Done) - } - pub fn reset(&self) -> Result<()> { EMBEDDED_FILES .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite) @@ -240,77 +110,3 @@ impl Display for Exercise { self.path.fmt(f) } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_pending_state() { - let exercise = Exercise { - name: "pending_exercise".into(), - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::new(), - }; - - let state = exercise.state(); - let expected = vec![ - ContextLine { - line: "// fake_exercise".to_string(), - number: 1, - important: false, - }, - ContextLine { - line: "".to_string(), - number: 2, - important: false, - }, - ContextLine { - line: "// I AM NOT DONE".to_string(), - number: 3, - important: true, - }, - ContextLine { - line: "".to_string(), - number: 4, - important: false, - }, - ContextLine { - line: "fn main() {".to_string(), - number: 5, - important: false, - }, - ]; - - assert_eq!(state.unwrap(), State::Pending(expected)); - } - - #[test] - fn test_finished_exercise() { - let exercise = Exercise { - name: "finished_exercise".into(), - path: PathBuf::from("tests/fixture/state/exercises/finished_exercise.rs"), - mode: Mode::Compile, - hint: String::new(), - }; - - assert_eq!(exercise.state().unwrap(), State::Done); - } - - #[test] - fn test_not_done() { - assert!(contains_not_done_comment("// I AM NOT DONE")); - assert!(contains_not_done_comment("/// I AM NOT DONE")); - assert!(contains_not_done_comment("// I AM NOT DONE")); - assert!(contains_not_done_comment("/// I AM NOT DONE")); - assert!(contains_not_done_comment("// I AM NOT DONE ")); - assert!(contains_not_done_comment("// I AM NOT DONE!")); - assert!(contains_not_done_comment("// I am not done")); - assert!(contains_not_done_comment("// i am NOT done")); - - assert!(!contains_not_done_comment("I AM NOT DONE")); - assert!(!contains_not_done_comment("// NOT DONE")); - assert!(!contains_not_done_comment("DONE")); - } -} diff --git a/src/list.rs b/src/list.rs index 560b85a8..80b78e8d 100644 --- a/src/list.rs +++ b/src/list.rs @@ -9,11 +9,11 @@ use std::{fmt::Write, io}; mod state; -use crate::{exercise::Exercise, state_file::StateFile}; +use crate::app_state::AppState; use self::state::{Filter, UiState}; -pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result<()> { +pub fn list(app_state: &mut AppState) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; enable_raw_mode()?; @@ -21,7 +21,7 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?; terminal.clear()?; - let mut ui_state = UiState::new(state_file, exercises); + let mut ui_state = UiState::new(app_state); 'outer: loop { terminal.draw(|frame| ui_state.draw(frame).unwrap())?; @@ -56,7 +56,7 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul "Enabled filter DONE │ Press d again to disable the filter" }; - ui_state = ui_state.with_updated_rows(state_file); + ui_state = ui_state.with_updated_rows(); ui_state.message.push_str(message); } KeyCode::Char('p') => { @@ -68,23 +68,20 @@ pub fn list(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Resul "Enabled filter PENDING │ Press p again to disable the filter" }; - ui_state = ui_state.with_updated_rows(state_file); + ui_state = ui_state.with_updated_rows(); ui_state.message.push_str(message); } KeyCode::Char('r') => { - let selected = ui_state.selected(); - let exercise = &exercises[selected]; - exercise.reset()?; - state_file.reset(selected)?; + let exercise = ui_state.reset_selected()?; - ui_state = ui_state.with_updated_rows(state_file); + ui_state = ui_state.with_updated_rows(); ui_state .message .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; } KeyCode::Char('c') => { - state_file.set_next_exercise_ind(ui_state.selected())?; - ui_state = ui_state.with_updated_rows(state_file); + ui_state.selected_to_current_exercise()?; + ui_state = ui_state.with_updated_rows(); } _ => (), } diff --git a/src/list/state.rs b/src/list/state.rs index 209374b1..7714268c 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -7,7 +7,7 @@ use ratatui::{ Frame, }; -use crate::{exercise::Exercise, progress_bar::progress_bar_ratatui, state_file::StateFile}; +use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui}; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -16,30 +16,29 @@ pub enum Filter { None, } -pub struct UiState { +pub struct UiState<'a> { pub table: Table<'static>, pub message: String, pub filter: Filter, - exercises: &'static [Exercise], - progress: u16, - selected: usize, + app_state: &'a mut AppState, table_state: TableState, + selected: usize, last_ind: usize, } -impl UiState { - pub fn with_updated_rows(mut self, state_file: &StateFile) -> Self { +impl<'a> UiState<'a> { + pub fn with_updated_rows(mut self) -> Self { + let current_exercise_ind = self.app_state.current_exercise_ind(); + let mut rows_counter: usize = 0; - let mut progress: u16 = 0; let rows = self - .exercises + .app_state + .exercises() .iter() - .zip(state_file.progress().iter().copied()) + .zip(self.app_state.progress().iter().copied()) .enumerate() .filter_map(|(ind, (exercise, done))| { let exercise_state = if done { - progress += 1; - if self.filter == Filter::Pending { return None; } @@ -55,7 +54,7 @@ impl UiState { rows_counter += 1; - let next = if ind == state_file.next_exercise_ind() { + let next = if ind == current_exercise_ind { ">>>>".bold().red() } else { Span::default() @@ -74,15 +73,14 @@ impl UiState { self.last_ind = rows_counter.saturating_sub(1); self.select(self.selected.min(self.last_ind)); - self.progress = progress; - self } - pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { + pub fn new(app_state: &'a mut AppState) -> Self { let header = Row::new(["Next", "State", "Name", "Path"]); - let max_name_len = exercises + let max_name_len = app_state + .exercises() .iter() .map(|exercise| exercise.name.len()) .max() @@ -104,7 +102,7 @@ impl UiState { .highlight_symbol("🦀") .block(Block::default().borders(Borders::BOTTOM)); - let selected = state_file.next_exercise_ind(); + let selected = app_state.current_exercise_ind(); let table_state = TableState::default() .with_offset(selected.saturating_sub(10)) .with_selected(Some(selected)); @@ -113,19 +111,13 @@ impl UiState { table, message: String::with_capacity(128), filter: Filter::None, - exercises, - progress: 0, - selected, + app_state, table_state, + selected, last_ind: 0, }; - slf.with_updated_rows(state_file) - } - - #[inline] - pub fn selected(&self) -> usize { - self.selected + slf.with_updated_rows() } fn select(&mut self, ind: usize) { @@ -134,11 +126,13 @@ impl UiState { } pub fn select_next(&mut self) { - self.select(self.selected.saturating_add(1).min(self.last_ind)); + let next = (self.selected + 1).min(self.last_ind); + self.select(next); } pub fn select_previous(&mut self) { - self.select(self.selected.saturating_sub(1)); + let previous = self.selected.saturating_sub(1); + self.select(previous); } #[inline] @@ -167,8 +161,8 @@ impl UiState { frame.render_widget( Paragraph::new(progress_bar_ratatui( - self.progress, - self.exercises.len() as u16, + self.app_state.n_done(), + self.app_state.exercises().len() as u16, area.width, )?) .block(Block::default().borders(Borders::BOTTOM)), @@ -200,4 +194,19 @@ impl UiState { Ok(()) } + + pub fn reset_selected(&mut self) -> Result<&'static Exercise> { + self.app_state.set_pending(self.selected)?; + // TODO: Take care of filters! + let exercise = &self.app_state.exercises()[self.selected]; + exercise.reset()?; + + Ok(exercise) + } + + #[inline] + pub fn selected_to_current_exercise(&mut self) -> Result<()> { + // TODO: Take care of filters! + self.app_state.set_current_exercise_ind(self.selected) + } } diff --git a/src/main.rs b/src/main.rs index fc83e0fd..926605c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use std::{path::Path, process::exit}; +mod app_state; mod consts; mod embedded; mod exercise; @@ -9,17 +10,15 @@ mod init; mod list; mod progress_bar; mod run; -mod state_file; -mod verify; mod watch; use self::{ + app_state::AppState, consts::WELCOME, - exercise::{Exercise, InfoFile}, + exercise::InfoFile, + init::init, list::list, run::run, - state_file::StateFile, - verify::{verify, VerifyState}, watch::{watch, WatchExit}, }; @@ -35,14 +34,12 @@ struct Args { enum Subcommands { /// Initialize Rustlings Init, - /// Verify all exercises according to the recommended order - Verify, /// Same as just running `rustlings` without a subcommand. Watch, - /// Run/Test a single exercise + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified. Run { /// The name of the exercise - name: String, + name: Option, }, /// Reset a single exercise Reset { @@ -56,26 +53,6 @@ enum Subcommands { }, } -fn find_exercise(name: &str, exercises: &'static [Exercise]) -> Result<(usize, &'static Exercise)> { - if name == "next" { - for (ind, exercise) in exercises.iter().enumerate() { - if !exercise.looks_done()? { - return Ok((ind, exercise)); - } - } - - println!("🎉 Congratulations! You have done all the exercises!"); - println!("🔚 There are no more exercises to do next!"); - exit(0); - } - - exercises - .iter() - .enumerate() - .find(|(_, exercise)| exercise.name == name) - .with_context(|| format!("No exercise found for '{name}'!")) -} - fn main() -> Result<()> { let args = Args::parse(); @@ -87,11 +64,10 @@ Try running `cargo --version` to diagnose the problem.", let mut info_file = InfoFile::parse()?; info_file.exercises.shrink_to_fit(); - // Leaking is not a problem since the exercises' slice is used until the end of the program. - let exercises = info_file.exercises.leak(); + let exercises = info_file.exercises; if matches!(args.command, Some(Subcommands::Init)) { - init::init(exercises).context("Initialization failed")?; + init(&exercises).context("Initialization failed")?; println!( "\nDone initialization!\n Run `cd rustlings` to go into the generated directory. @@ -109,38 +85,37 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini exit(1); } - let mut state_file = StateFile::read_or_default(exercises); + let mut app_state = AppState::new(exercises); match args.command { None | Some(Subcommands::Watch) => loop { - match watch(&mut state_file, exercises)? { + match watch(&mut app_state)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the // watch state. - WatchExit::List => list(&mut state_file, exercises)?, + WatchExit::List => list(&mut app_state)?, } }, // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { - let (_, exercise) = find_exercise(&name, exercises)?; - run(exercise).unwrap_or_else(|_| exit(1)); + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; + } + run(&mut app_state)?; } Some(Subcommands::Reset { name }) => { - let (ind, exercise) = find_exercise(&name, exercises)?; + app_state.set_current_exercise_by_name(&name)?; + app_state.set_pending(app_state.current_exercise_ind())?; + let exercise = app_state.current_exercise(); exercise.reset()?; - state_file.reset(ind)?; println!("The exercise {exercise} has been reset!"); } Some(Subcommands::Hint { name }) => { - let (_, exercise) = find_exercise(&name, exercises)?; - println!("{}", exercise.hint); + app_state.set_current_exercise_by_name(&name)?; + println!("{}", app_state.current_exercise().hint); } - Some(Subcommands::Verify) => match verify(exercises, 0)? { - VerifyState::AllExercisesDone => println!("All exercises done!"), - VerifyState::Failed(exercise) => bail!("Exercise {exercise} failed"), - }, } Ok(()) diff --git a/src/run.rs b/src/run.rs index 2fd6f407..18da193b 100644 --- a/src/run.rs +++ b/src/run.rs @@ -2,13 +2,10 @@ use anyhow::{bail, Result}; use crossterm::style::Stylize; use std::io::{stdout, Write}; -use crate::exercise::Exercise; +use crate::app_state::{AppState, ExercisesProgress}; -// Invoke the rust compiler on the path of the given exercise, -// and run the ensuing binary. -// The verbose argument helps determine whether or not to show -// the output from the test harnesses (if the mode of the exercise is test) -pub fn run(exercise: &Exercise) -> Result<()> { +pub fn run(app_state: &mut AppState) -> Result<()> { + let exercise = app_state.current_exercise(); let output = exercise.run()?; { @@ -22,7 +19,19 @@ pub fn run(exercise: &Exercise) -> Result<()> { bail!("Ran {exercise} with errors"); } - println!("{}", "✓ Successfully ran {exercise}".green()); + println!( + "{}{}", + "✓ Successfully ran ".green(), + exercise.path.to_string_lossy().green(), + ); + + match app_state.done_current_exercise()? { + ExercisesProgress::AllDone => println!( + "🎉 Congratulations! You have done all the exercises! +🔚 There are no more exercises to do next!" + ), + ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), + } Ok(()) } diff --git a/src/state_file.rs b/src/state_file.rs deleted file mode 100644 index 6b80354e..00000000 --- a/src/state_file.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anyhow::{bail, Context, Result}; -use serde::{Deserialize, Serialize}; -use std::fs; - -use crate::exercise::Exercise; - -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct StateFile { - next_exercise_ind: usize, - progress: Vec, -} - -const BAD_INDEX_ERR: &str = "The next exercise index is higher than the number of exercises"; - -impl StateFile { - fn read(exercises: &[Exercise]) -> Option { - let file_content = fs::read(".rustlings-state.json").ok()?; - - let slf: Self = serde_json::de::from_slice(&file_content).ok()?; - - if slf.progress.len() != exercises.len() || slf.next_exercise_ind >= exercises.len() { - return None; - } - - Some(slf) - } - - pub fn read_or_default(exercises: &[Exercise]) -> Self { - Self::read(exercises).unwrap_or_else(|| Self { - next_exercise_ind: 0, - progress: vec![false; exercises.len()], - }) - } - - fn write(&self) -> Result<()> { - let mut buf = Vec::with_capacity(1024); - serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - fs::write(".rustlings-state.json", buf) - .context("Failed to write the state file `.rustlings-state.json`")?; - - Ok(()) - } - - #[inline] - pub fn next_exercise_ind(&self) -> usize { - self.next_exercise_ind - } - - pub fn set_next_exercise_ind(&mut self, ind: usize) -> Result<()> { - if ind >= self.progress.len() { - bail!(BAD_INDEX_ERR); - } - self.next_exercise_ind = ind; - self.write() - } - - #[inline] - pub fn progress(&self) -> &[bool] { - &self.progress - } - - pub fn reset(&mut self, ind: usize) -> Result<()> { - let done = self.progress.get_mut(ind).context(BAD_INDEX_ERR)?; - *done = false; - self.write() - } -} diff --git a/src/verify.rs b/src/verify.rs deleted file mode 100644 index cea6bdf6..00000000 --- a/src/verify.rs +++ /dev/null @@ -1,85 +0,0 @@ -use anyhow::Result; -use crossterm::style::{Attribute, ContentStyle, Stylize}; -use std::io::{stdout, Write}; - -use crate::exercise::{Exercise, Mode, State}; - -pub enum VerifyState { - AllExercisesDone, - Failed(&'static Exercise), -} - -// Verify that the provided container of Exercise objects -// can be compiled and run without any failures. -// Any such failures will be reported to the end user. -// If the Exercise being verified is a test, the verbose boolean -// determines whether or not the test harness outputs are displayed. -pub fn verify( - exercises: &'static [Exercise], - mut current_exercise_ind: usize, -) -> Result { - while current_exercise_ind < exercises.len() { - let exercise = &exercises[current_exercise_ind]; - - println!( - "Progress: {current_exercise_ind}/{} ({:.1}%)\n", - exercises.len(), - current_exercise_ind as f32 / exercises.len() as f32 * 100.0, - ); - - let output = exercise.run()?; - - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } - - if !output.status.success() { - return Ok(VerifyState::Failed(exercise)); - } - - println!(); - // TODO: Color - match exercise.mode { - Mode::Compile => println!("Successfully ran {exercise}!"), - Mode::Test => println!("Successfully tested {exercise}!"), - Mode::Clippy => println!("Successfully checked {exercise}!"), - } - - if let State::Pending(context) = exercise.state()? { - println!( - "\nYou can keep working on this exercise, -or jump into the next one by removing the {} comment:\n", - "`I AM NOT DONE`".bold() - ); - - for context_line in context { - let formatted_line = if context_line.important { - format!("{}", context_line.line.bold()) - } else { - context_line.line - }; - - println!( - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - ); - } - return Ok(VerifyState::Failed(exercise)); - } - - current_exercise_ind += 1; - } - - Ok(VerifyState::AllExercisesDone) -} diff --git a/src/watch.rs b/src/watch.rs index b29169b3..929275f1 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -15,7 +15,7 @@ mod debounce_event; mod state; mod terminal_event; -use crate::{exercise::Exercise, state_file::StateFile}; +use crate::app_state::AppState; use self::{ debounce_event::DebounceEventHandler, @@ -39,23 +39,23 @@ pub enum WatchExit { List, } -pub fn watch(state_file: &mut StateFile, exercises: &'static [Exercise]) -> Result { +pub fn watch(app_state: &mut AppState) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), DebounceEventHandler { tx: tx.clone(), - exercises, + exercises: app_state.exercises(), }, )?; debouncer .watcher() .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let mut watch_state = WatchState::new(state_file, exercises); + let mut watch_state = WatchState::new(app_state); // TODO: bool - watch_state.run_exercise()?; + watch_state.run_current_exercise()?; watch_state.render()?; thread::spawn(move || terminal_event_handler(tx)); diff --git a/src/watch/state.rs b/src/watch/state.rs index 6f6d2f10..a7647d8d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,26 +1,16 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use crossterm::{ - style::{Attribute, ContentStyle, Stylize}, + style::Stylize, terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use std::{ - fmt::Write as _, - io::{self, StdoutLock, Write}, -}; +use std::io::{self, StdoutLock, Write}; -use crate::{ - exercise::{Exercise, State}, - progress_bar::progress_bar, - state_file::StateFile, -}; +use crate::{app_state::AppState, progress_bar::progress_bar}; pub struct WatchState<'a> { writer: StdoutLock<'a>, - exercises: &'static [Exercise], - exercise: &'static Exercise, - current_exercise_ind: usize, - progress: u16, + app_state: &'a mut AppState, stdout: Option>, stderr: Option>, message: Option, @@ -28,19 +18,12 @@ pub struct WatchState<'a> { } impl<'a> WatchState<'a> { - pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self { - let current_exercise_ind = state_file.next_exercise_ind(); - let progress = state_file.progress().iter().filter(|done| **done).count() as u16; - let exercise = &exercises[current_exercise_ind]; - + pub fn new(app_state: &'a mut AppState) -> Self { let writer = io::stdout().lock(); Self { writer, - exercises, - exercise, - current_exercise_ind, - progress, + app_state, stdout: None, stderr: None, message: None, @@ -53,8 +36,8 @@ impl<'a> WatchState<'a> { self.writer } - pub fn run_exercise(&mut self) -> Result { - let output = self.exercise.run()?; + pub fn run_current_exercise(&mut self) -> Result { + let output = self.app_state.current_exercise().run()?; self.stdout = Some(output.stdout); if !output.status.success() { @@ -64,55 +47,15 @@ impl<'a> WatchState<'a> { self.stderr = None; - if let State::Pending(context) = self.exercise.state()? { - let mut message = format!( - " -You can keep working on this exercise or jump into the next one by removing the {} comment: - -", - "`I AM NOT DONE`".bold(), - ); - - for context_line in context { - let formatted_line = if context_line.important { - context_line.line.bold() - } else { - context_line.line.stylize() - }; - - writeln!( - message, - "{:>2} {} {}", - ContentStyle { - foreground_color: Some(crossterm::style::Color::Blue), - background_color: None, - underline_color: None, - attributes: Attribute::Bold.into() - } - .apply(context_line.number), - "|".blue(), - formatted_line, - )?; - } - - self.message = Some(message); - return Ok(false); - } - Ok(true) } pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result { - self.exercise = self - .exercises - .get(exercise_ind) - .context("Invalid exercise index")?; - self.current_exercise_ind = exercise_ind; - - self.run_exercise() + self.app_state.set_current_exercise_ind(exercise_ind)?; + self.run_current_exercise() } - pub fn show_prompt(&mut self) -> io::Result<()> { + fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n\n")?; if !self.hint_displayed { @@ -150,18 +93,27 @@ You can keep working on this exercise or jump into the next one by removing the if self.hint_displayed { self.writer .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?; - self.writer.write_all(self.exercise.hint.as_bytes())?; + self.writer + .write_all(self.app_state.current_exercise().hint.as_bytes())?; self.writer.write_all(b"\n\n")?; } let line_width = size()?.0; - let progress_bar = progress_bar(self.progress, self.exercises.len() as u16, line_width)?; + let progress_bar = progress_bar( + self.app_state.n_done(), + self.app_state.exercises().len() as u16, + line_width, + )?; self.writer.write_all(progress_bar.as_bytes())?; self.writer.write_all(b"Current exercise: ")?; self.writer.write_fmt(format_args!( "{}", - self.exercise.path.to_string_lossy().bold() + self.app_state + .current_exercise() + .path + .to_string_lossy() + .bold(), ))?; self.show_prompt()?; diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs index f579d0b4..016b827c 100644 --- a/tests/fixture/state/exercises/pending_exercise.rs +++ b/tests/fixture/state/exercises/pending_exercise.rs @@ -1,7 +1,5 @@ // fake_exercise -// I AM NOT DONE - fn main() { } diff --git a/tests/fixture/state/exercises/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs index 8756f02d..2002ef17 100644 --- a/tests/fixture/state/exercises/pending_test_exercise.rs +++ b/tests/fixture/state/exercises/pending_test_exercise.rs @@ -1,4 +1,2 @@ -// I AM NOT DONE - #[test] fn it_works() {} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f8f4383f..51cdefb8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,7 +1,6 @@ use assert_cmd::prelude::*; -use glob::glob; use predicates::boolean::PredicateBooleanExt; -use std::{fs::File, io::Read, process::Command}; +use std::process::Command; #[test] fn fails_when_in_wrong_dir() { @@ -137,31 +136,6 @@ fn get_hint_for_single_test() { .stdout("Hello!\n"); } -#[test] -fn all_exercises_require_confirmation() { - for exercise in glob("exercises/**/*.rs").unwrap() { - let path = exercise.unwrap(); - if path.file_name().unwrap() == "mod.rs" { - continue; - } - let source = { - let mut file = File::open(&path).unwrap(); - let mut s = String::new(); - file.read_to_string(&mut s).unwrap(); - s - }; - source - .matches("// I AM NOT DONE") - .next() - .unwrap_or_else(|| { - panic!( - "There should be an `I AM NOT DONE` annotation in {:?}", - path - ) - }); - } -} - #[test] fn run_compile_exercise_does_not_prompt() { Command::cargo_bin("rustlings") From 65849629f5877a5d9f51accbb593d431938bd60c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 02:51:23 +0200 Subject: [PATCH 106/433] Remove glob --- Cargo.lock | 7 ------- Cargo.toml | 1 - 2 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aeb6c61f..a8ffb8ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,12 +320,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "hashbrown" version = "0.14.3" @@ -690,7 +684,6 @@ dependencies = [ "assert_cmd", "clap", "crossterm", - "glob", "notify-debouncer-mini", "predicates", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index 435dfd49..83f01c25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,6 @@ which = "6.0.1" [dev-dependencies] assert_cmd = "2.0.14" -glob = "0.3.0" predicates = "3.1.0" [profile.release] From c3933904f643238eaafe42e7da967c8262fef22a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 02:51:50 +0200 Subject: [PATCH 107/433] Update deps --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- rustlings-macros/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8ffb8ec..554db288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "assert_cmd" @@ -598,9 +598,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] diff --git a/Cargo.toml b/Cargo.toml index 83f01c25..285e7df6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ license = "MIT" edition = "2021" [workspace.dependencies] -anyhow = "1.0.81" +anyhow = "1.0.82" serde = { version = "1.0.197", features = ["derive"] } toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 0114c8f0..79279f57 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -9,4 +9,4 @@ edition.workspace = true proc-macro = true [dependencies] -quote = "1.0.35" +quote = "1.0.36" From 686143100fbb89e2a7ba4098134fe37bf0c69ad2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 02:55:58 +0200 Subject: [PATCH 108/433] Update intro1 --- exercises/00_intro/intro1.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index aa505a13..e4e0444a 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -27,13 +27,6 @@ fn main() { println!("or logic error. The central concept behind Rustlings is to fix these errors and"); println!("solve the exercises. Good luck!"); println!(); - println!("The source for this exercise is in `exercises/00_intro/intro1.rs`. Have a look!"); - println!( - "Going forward, the source of the exercises will always be in the success/failure output." - ); - println!(); - println!( - "If you want to use rust-analyzer, Rust's LSP implementation, make sure your editor is set" - ); - println!("up, and then run `rustlings lsp` before continuing.") + println!("The file of this exercise is `exercises/00_intro/intro1.rs`. Have a look!"); + println!("The current exercise path is shown under the progress bar in the watch mode."); } From 470dc65956dae034f17deefbc0b45490e1ec1448 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 14:35:30 +0200 Subject: [PATCH 109/433] Fix selected when there are no rows --- src/list.rs | 4 ++- src/list/state.rs | 75 +++++++++++++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/list.rs b/src/list.rs index 80b78e8d..de120eaf 100644 --- a/src/list.rs +++ b/src/list.rs @@ -72,7 +72,9 @@ pub fn list(app_state: &mut AppState) -> Result<()> { ui_state.message.push_str(message); } KeyCode::Char('r') => { - let exercise = ui_state.reset_selected()?; + let Some(exercise) = ui_state.reset_selected()? else { + continue; + }; ui_state = ui_state.with_updated_rows(); ui_state diff --git a/src/list/state.rs b/src/list/state.rs index 7714268c..3344fbbd 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -22,15 +22,14 @@ pub struct UiState<'a> { pub filter: Filter, app_state: &'a mut AppState, table_state: TableState, - selected: usize, - last_ind: usize, + n_rows: usize, } impl<'a> UiState<'a> { pub fn with_updated_rows(mut self) -> Self { let current_exercise_ind = self.app_state.current_exercise_ind(); - let mut rows_counter: usize = 0; + self.n_rows = 0; let rows = self .app_state .exercises() @@ -52,7 +51,7 @@ impl<'a> UiState<'a> { "PENDING".yellow() }; - rows_counter += 1; + self.n_rows += 1; let next = if ind == current_exercise_ind { ">>>>".bold().red() @@ -70,8 +69,15 @@ impl<'a> UiState<'a> { self.table = self.table.rows(rows); - self.last_ind = rows_counter.saturating_sub(1); - self.select(self.selected.min(self.last_ind)); + if self.n_rows == 0 { + self.table_state.select(None); + } else { + self.table_state.select(Some( + self.table_state + .selected() + .map_or(0, |selected| selected.min(self.n_rows - 1)), + )); + } self } @@ -107,42 +113,53 @@ impl<'a> UiState<'a> { .with_offset(selected.saturating_sub(10)) .with_selected(Some(selected)); + let filter = Filter::None; + let n_rows = app_state.exercises().len(); + let slf = Self { table, message: String::with_capacity(128), - filter: Filter::None, + filter, app_state, table_state, - selected, - last_ind: 0, + n_rows, }; slf.with_updated_rows() } - fn select(&mut self, ind: usize) { - self.selected = ind; - self.table_state.select(Some(ind)); - } - pub fn select_next(&mut self) { - let next = (self.selected + 1).min(self.last_ind); - self.select(next); + if self.n_rows > 0 { + let next = self + .table_state + .selected() + .map_or(0, |selected| (selected + 1).min(self.n_rows - 1)); + self.table_state.select(Some(next)); + } } pub fn select_previous(&mut self) { - let previous = self.selected.saturating_sub(1); - self.select(previous); + if self.n_rows > 0 { + let previous = self + .table_state + .selected() + .map_or(0, |selected| selected.saturating_sub(1)); + self.table_state.select(Some(previous)); + } } #[inline] pub fn select_first(&mut self) { - self.select(0); + if self.n_rows > 0 { + self.table_state.select(Some(0)); + } } #[inline] pub fn select_last(&mut self) { - self.select(self.last_ind); + if self.n_rows > 0 { + self.table_state.select(Some(self.n_rows - 1)); + } } pub fn draw(&mut self, frame: &mut Frame) -> Result<()> { @@ -195,18 +212,26 @@ impl<'a> UiState<'a> { Ok(()) } - pub fn reset_selected(&mut self) -> Result<&'static Exercise> { - self.app_state.set_pending(self.selected)?; + pub fn reset_selected(&mut self) -> Result> { + let Some(selected) = self.table_state.selected() else { + return Ok(None); + }; + + self.app_state.set_pending(selected)?; // TODO: Take care of filters! - let exercise = &self.app_state.exercises()[self.selected]; + let exercise = &self.app_state.exercises()[selected]; exercise.reset()?; - Ok(exercise) + Ok(Some(exercise)) } #[inline] pub fn selected_to_current_exercise(&mut self) -> Result<()> { + let Some(selected) = self.table_state.selected() else { + return Ok(()); + }; + // TODO: Take care of filters! - self.app_state.set_current_exercise_ind(self.selected) + self.app_state.set_current_exercise_ind(selected) } } From f53a0e870045ac0ff1bb4a3be7fe125680d477a5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 14:39:19 +0200 Subject: [PATCH 110/433] Panic if there are no exercises --- src/exercise.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index de435d13..f01c6fcc 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -31,12 +31,21 @@ impl InfoFile { pub fn parse() -> Result { // Read a local `info.toml` if it exists. // Mainly to let the tests work for now. - if let Ok(file_content) = fs::read_to_string("info.toml") { + let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") { toml_edit::de::from_str(&file_content) } else { toml_edit::de::from_str(include_str!("../info.toml")) } - .context("Failed to parse `info.toml`") + .context("Failed to parse `info.toml`")?; + + if slf.exercises.is_empty() { + panic!( + "There are no exercises yet! +If you are developing third-party exercises, add at least one exercise before testing." + ); + } + + Ok(slf) } } From 2e1a87d7d3671c82932eb63b38ba383ce1fc7d53 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 14:58:56 +0200 Subject: [PATCH 111/433] Take care of filters when resolving the selected exercise --- src/list/state.rs | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 3344fbbd..0dcfe88a 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use ratatui::{ layout::{Constraint, Rect}, style::{Style, Stylize}, @@ -217,21 +217,44 @@ impl<'a> UiState<'a> { return Ok(None); }; - self.app_state.set_pending(selected)?; - // TODO: Take care of filters! - let exercise = &self.app_state.exercises()[selected]; + let (ind, exercise) = self + .app_state + .exercises() + .iter() + .zip(self.app_state.progress()) + .enumerate() + .filter_map(|(ind, (exercise, done))| match self.filter { + Filter::Done => done.then_some((ind, exercise)), + Filter::Pending => (!done).then_some((ind, exercise)), + Filter::None => Some((ind, exercise)), + }) + .nth(selected) + .context("Invalid selection index")?; + + self.app_state.set_pending(ind)?; exercise.reset()?; Ok(Some(exercise)) } - #[inline] pub fn selected_to_current_exercise(&mut self) -> Result<()> { let Some(selected) = self.table_state.selected() else { return Ok(()); }; - // TODO: Take care of filters! - self.app_state.set_current_exercise_ind(selected) + let ind = self + .app_state + .progress() + .iter() + .enumerate() + .filter_map(|(ind, done)| match self.filter { + Filter::Done => done.then_some(ind), + Filter::Pending => (!done).then_some(ind), + Filter::None => Some(ind), + }) + .nth(selected) + .context("Invalid selection index")?; + + self.app_state.set_current_exercise_ind(ind) } } From e79bc727f07bbe99092f30e66f4df845a2cd2ec5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 15:08:46 +0200 Subject: [PATCH 112/433] Don't listen on keys with modifiers --- src/list.rs | 16 +++++++++++----- src/watch/terminal_event.rs | 6 +++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/list.rs b/src/list.rs index de120eaf..2430ed73 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,6 +1,6 @@ use anyhow::Result; use crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, + event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; @@ -28,10 +28,16 @@ pub fn list(app_state: &mut AppState) -> Result<()> { let key = loop { match event::read()? { - Event::Key(key) => match key.kind { - KeyEventKind::Press | KeyEventKind::Repeat => break key, - KeyEventKind::Release => (), - }, + Event::Key(key) => { + if key.modifiers != KeyModifiers::NONE { + continue; + } + + match key.kind { + KeyEventKind::Press | KeyEventKind::Repeat => break key, + KeyEventKind::Release => (), + } + } // Redraw Event::Resize(_, _) => continue 'outer, // Ignore diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 7c85b5bd..faca8a2b 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,4 +1,4 @@ -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use std::sync::mpsc::Sender; use super::WatchEvent; @@ -26,6 +26,10 @@ pub fn terminal_event_handler(tx: Sender) { match terminal_event { Event::Key(key) => { + if key.modifiers != KeyModifiers::NONE { + continue; + } + match key.kind { KeyEventKind::Release => continue, KeyEventKind::Press | KeyEventKind::Repeat => (), From 864cfa725be9dc78b1b962f13c8b6a0bc971d4c4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 15:10:15 +0200 Subject: [PATCH 113/433] Remove outdated tests --- tests/integration_tests.rs | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 51cdefb8..f81cc94b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -11,26 +11,6 @@ fn fails_when_in_wrong_dir() { .code(1); } -#[test] -fn verify_all_success() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("verify") - .current_dir("tests/fixture/success") - .assert() - .success(); -} - -#[test] -fn verify_fails_if_some_fails() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("verify") - .current_dir("tests/fixture/failure") - .assert() - .code(1); -} - #[test] fn run_single_compile_success() { Command::cargo_bin("rustlings") @@ -81,19 +61,6 @@ fn run_single_test_not_passed() { .code(1); } -#[test] -fn run_single_test_no_filename() { - Command::cargo_bin("rustlings") - .unwrap() - .arg("run") - .current_dir("tests/fixture/") - .assert() - .code(2) - .stderr(predicates::str::contains( - "required arguments were not provided", - )); -} - #[test] fn run_single_test_no_exercise() { Command::cargo_bin("rustlings") From 6494a8c50be2e3b8fbd9bb0ae50d8dfbf0569e2a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 11 Apr 2024 16:54:27 +0200 Subject: [PATCH 114/433] Remove the watch subcommand --- src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 926605c1..7bc10ac8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,8 +34,6 @@ struct Args { enum Subcommands { /// Initialize Rustlings Init, - /// Same as just running `rustlings` without a subcommand. - Watch, /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified. Run { /// The name of the exercise @@ -88,7 +86,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini let mut app_state = AppState::new(exercises); match args.command { - None | Some(Subcommands::Watch) => loop { + None => loop { match watch(&mut app_state)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart From d8160f9113ea4f896c0843a40b9444a6e175826f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 00:56:40 +0200 Subject: [PATCH 115/433] Remove outdated installation methods --- .devcontainer/devcontainer.json | 8 ---- .gitignore | 4 -- .gitpod.yml | 7 --- .vscode/extensions.json | 5 --- README.md | 77 +------------------------------- flake.lock | 78 --------------------------------- flake.nix | 78 --------------------------------- shell.nix | 6 --- 8 files changed, 1 insertion(+), 262 deletions(-) delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .gitpod.yml delete mode 100644 .vscode/extensions.json delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 shell.nix diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index f25e8bd8..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "image": "mcr.microsoft.com/devcontainers/rust:1", - "updateContentCommand": ["cargo", "build"], - "postAttachCommand": ["rustlings", "watch"], - "remoteEnv": { - "PATH": "${containerEnv:PATH}:${containerWorkspaceFolder}/target/debug" - } -} diff --git a/.gitignore b/.gitignore index 2d4a04dc..c9172e01 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,5 @@ public/ .idea *.iml -# VS Code extension recommendations -.vscode/* -!.vscode/extensions.json - # Ignore file for editors like Helix .ignore diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 06919335..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,7 +0,0 @@ -tasks: - - init: /workspace/rustlings/install.sh - command: /workspace/.cargo/bin/rustlings watch - -vscode: - extensions: - - rust-lang.rust-analyzer@0.3.1348 diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index b85de749..00000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "rust-lang.rust-analyzer" - ] -} diff --git a/README.md b/README.md index fd76fdff..96421ebd 100644 --- a/README.md +++ b/README.md @@ -18,78 +18,7 @@ _Note: If you're on Linux, make sure you've installed gcc. Deb: `sudo apt instal You will need to have Rust installed. You can get it by visiting . This'll also install Cargo, Rust's package/project manager. -## MacOS/Linux - -Just run: - -```bash -curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -``` - -Or if you want it to be installed to a different path: - -```bash -curl -L https://raw.githubusercontent.com/rust-lang/rustlings/main/install.sh | bash -s mypath/ -``` - -This will install Rustlings and give you access to the `rustlings` command. Run it to get started! - -### Nix - -Basically: Clone the repository at the latest tag, finally run `nix develop` or `nix-shell`. - -```bash -# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1) -git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings -cd rustlings -# if nix version > 2.3 -nix develop -# if nix version <= 2.3 -nix-shell -``` - -## Windows - -In PowerShell (Run as Administrator), set `ExecutionPolicy` to `RemoteSigned`: - -```ps1 -Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -``` - -Then, you can run: - -```ps1 -Start-BitsTransfer -Source https://raw.githubusercontent.com/rust-lang/rustlings/main/install.ps1 -Destination $env:TMP/install_rustlings.ps1; Unblock-File $env:TMP/install_rustlings.ps1; Invoke-Expression $env:TMP/install_rustlings.ps1 -``` - -To install Rustlings. Same as on MacOS/Linux, you will have access to the `rustlings` command after it. Keep in mind that this works best in PowerShell, and any other terminals may give you errors. - -If you get a permission denied message, you might have to exclude the directory where you cloned Rustlings in your antivirus. - -## Browser - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rust-lang/rustlings) - -[![Open Rustlings On Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new/?repo=rust-lang%2Frustlings&ref=main) - -## Manually - -Basically: Clone the repository at the latest tag, run `cargo install --path .`. - -```bash -# find out the latest version at https://github.com/rust-lang/rustlings/releases/latest (on edit 5.6.1) -git clone -b 5.6.1 --depth 1 https://github.com/rust-lang/rustlings -cd rustlings -cargo install --force --path . -``` - -If there are installation errors, ensure that your toolchain is up to date. For the latest, run: - -```bash -rustup update -``` - -Then, same as above, run `rustlings` to get started. + ## Doing exercises @@ -138,10 +67,6 @@ rustlings list After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`. -## Enabling `rust-analyzer` - -Run the command `rustlings lsp` which will generate a `rust-project.json` at the root of the project, this allows [rust-analyzer](https://rust-analyzer.github.io/) to parse each exercise. - ## Continuing On Once you've completed Rustlings, put your new knowledge to good use! Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 15238981..00000000 --- a/flake.lock +++ /dev/null @@ -1,78 +0,0 @@ -{ - "nodes": { - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1692799911, - "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1694183432, - "narHash": "sha256-YyPGNapgZNNj51ylQMw9lAgvxtM2ai1HZVUu3GS8Fng=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "db9208ab987cdeeedf78ad9b4cf3c55f5ebd269b", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 152d38e6..00000000 --- a/flake.nix +++ /dev/null @@ -1,78 +0,0 @@ -{ - description = "Small exercises to get you used to reading and writing Rust code"; - - inputs = { - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; - }; - flake-utils.url = "github:numtide/flake-utils"; - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - }; - - outputs = { self, flake-utils, nixpkgs, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - - cargoBuildInputs = with pkgs; lib.optionals stdenv.isDarwin [ - darwin.apple_sdk.frameworks.CoreServices - ]; - - rustlings = - pkgs.rustPlatform.buildRustPackage { - name = "rustlings"; - version = "5.6.1"; - - buildInputs = cargoBuildInputs; - nativeBuildInputs = [pkgs.git]; - - src = with pkgs.lib; cleanSourceWith { - src = self; - # a function that returns a bool determining if the path should be included in the cleaned source - filter = path: type: - let - # filename - baseName = builtins.baseNameOf (toString path); - # path from root directory - path' = builtins.replaceStrings [ "${self}/" ] [ "" ] path; - # checks if path is in the directory - inDirectory = directory: hasPrefix directory path'; - in - inDirectory "src" || - inDirectory "tests" || - hasPrefix "Cargo" baseName || - baseName == "info.toml"; - }; - - cargoLock.lockFile = ./Cargo.lock; - }; - in - { - devShell = pkgs.mkShell { - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - - buildInputs = with pkgs; [ - cargo - rustc - rust-analyzer - rustlings - rustfmt - clippy - ] ++ cargoBuildInputs; - }; - apps = let - rustlings-app = { - type = "app"; - program = "${rustlings}/bin/rustlings"; - }; - in { - default = rustlings-app; - rustlings = rustlings-app; - }; - packages = { - inherit rustlings; - default = rustlings; - }; - }); -} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index fa2a56c7..00000000 --- a/shell.nix +++ /dev/null @@ -1,6 +0,0 @@ -(import (let lock = builtins.fromJSON (builtins.readFile ./flake.lock); -in fetchTarball { - url = - "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; -}) { src = ./.; }).shellNix From 1e3745ccdf5ca41ae47d4f4d8594e8070df200a5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 00:58:26 +0200 Subject: [PATCH 116/433] Update winnow --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 554db288..a5ad8c9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1130,9 +1130,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" dependencies = [ "memchr", ] From 2a95a3e96644a0f769019204a518816c9f2e2aee Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 01:24:01 +0200 Subject: [PATCH 117/433] Deal with long strings --- info.toml | 32 ++++++++++++++++++++++++ src/consts.rs | 59 -------------------------------------------- src/exercise.rs | 12 ++++++--- src/init.rs | 40 ++++++++++++++++-------------- src/main.rs | 65 ++++++++++++++++++++++++++++++++++--------------- src/watch.rs | 10 +++++--- 6 files changed, 114 insertions(+), 104 deletions(-) delete mode 100644 src/consts.rs diff --git a/info.toml b/info.toml index c085e89c..d35b5702 100644 --- a/info.toml +++ b/info.toml @@ -1,3 +1,35 @@ +welcome_message = """Is this your first time? Don't worry, Rustlings was made for beginners! We are +going to teach you a lot of things about Rust, but before we can get +started, here's a couple of notes about how Rustlings operates: + +1. The central concept behind Rustlings is that you solve exercises. These + exercises usually have some sort of syntax error in them, which will cause + them to fail compilation or testing. Sometimes there's a logic error instead + of a syntax error. No matter what error, it's your job to find it and fix it! + You'll know when you fixed it because then, the exercise will compile and + Rustlings will be able to move on to the next exercise. +2. If you run Rustlings in watch mode (which we recommend), it'll automatically + start with the first exercise. Don't get confused by an error message popping + up as soon as you run Rustlings! This is part of the exercise that you're + supposed to solve, so open the exercise file in an editor and start your + detective work! +3. If you're stuck on an exercise, there is a helpful hint you can view by typing + 'hint' (in watch mode), or running `rustlings hint exercise_name`. +4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! + (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, + and sometimes, other learners do too so you can help each other out! + +Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. +Make sure to have your editor open in the `rustlings` directory!""" + +final_message = """We hope you enjoyed learning about the various aspects of Rust! +If you noticed any issues, please don't hesitate to report them to our repo. +You can also contribute your own exercises to help the greater community! + +Before reporting an issue or contributing, please read our guidelines: +https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md +""" + # INTRO [[exercises]] diff --git a/src/consts.rs b/src/consts.rs deleted file mode 100644 index 40bf150f..00000000 --- a/src/consts.rs +++ /dev/null @@ -1,59 +0,0 @@ -pub const WELCOME: &str = r" welcome to... - _ _ _ - _ __ _ _ ___| |_| (_)_ __ __ _ ___ - | '__| | | / __| __| | | '_ \ / _` / __| - | | | |_| \__ \ |_| | | | | | (_| \__ \ - |_| \__,_|___/\__|_|_|_| |_|\__, |___/ - |___/"; - -pub const DEFAULT_OUT: &str = - "Is this your first time? Don't worry, Rustlings was made for beginners! We are -going to teach you a lot of things about Rust, but before we can get -started, here's a couple of notes about how Rustlings operates: - -1. The central concept behind Rustlings is that you solve exercises. These - exercises usually have some sort of syntax error in them, which will cause - them to fail compilation or testing. Sometimes there's a logic error instead - of a syntax error. No matter what error, it's your job to find it and fix it! - You'll know when you fixed it because then, the exercise will compile and - Rustlings will be able to move on to the next exercise. -2. If you run Rustlings in watch mode (which we recommend), it'll automatically - start with the first exercise. Don't get confused by an error message popping - up as soon as you run Rustlings! This is part of the exercise that you're - supposed to solve, so open the exercise file in an editor and start your - detective work! -3. If you're stuck on an exercise, there is a helpful hint you can view by typing - 'hint' (in watch mode), or running `rustlings hint exercise_name`. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, - and sometimes, other learners do too so you can help each other out! - -Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the `rustlings` directory!"; - -pub const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/\x1b[31m - ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ - ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ - ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ - ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ - ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m - -We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, please don't hesitate to report them to our repo. -You can also contribute your own exercises to help the greater community! - -Before reporting an issue or contributing, please read our guidelines: -https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md"; diff --git a/src/exercise.rs b/src/exercise.rs index f01c6fcc..d28f4dbe 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -24,6 +24,10 @@ pub enum Mode { #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct InfoFile { + // TODO + pub welcome_message: Option, + // TODO + pub final_message: Option, pub exercises: Vec, } @@ -39,10 +43,7 @@ impl InfoFile { .context("Failed to parse `info.toml`")?; if slf.exercises.is_empty() { - panic!( - "There are no exercises yet! -If you are developing third-party exercises, add at least one exercise before testing." - ); + panic!("{NO_EXERCISES_ERR}"); } Ok(slf) @@ -119,3 +120,6 @@ impl Display for Exercise { self.path.fmt(f) } } + +const NO_EXERCISES_ERR: &str = "There are no exercises yet! +If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/init.rs b/src/init.rs index bc561eaf..44747438 100644 --- a/src/init.rs +++ b/src/init.rs @@ -36,47 +36,33 @@ publish = false } fn create_gitignore() -> io::Result<()> { - let gitignore = b"/target -/.rustlings-state.json"; OpenOptions::new() .create_new(true) .write(true) .open(".gitignore")? - .write_all(gitignore) + .write_all(GITIGNORE) } fn create_vscode_dir() -> Result<()> { create_dir(".vscode").context("Failed to create the directory `.vscode`")?; - let vs_code_extensions_json = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; OpenOptions::new() .create_new(true) .write(true) .open(".vscode/extensions.json")? - .write_all(vs_code_extensions_json)?; + .write_all(VS_CODE_EXTENSIONS_JSON)?; Ok(()) } pub fn init(exercises: &[Exercise]) -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { - bail!( - "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist -in the current directory. It looks like Rustlings was already initialized here. -Run `rustlings` for instructions on getting started with the exercises. - -If you didn't already initialize Rustlings, please initialize it in another directory." - ); + bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); } let rustlings_path = Path::new("rustlings"); if let Err(e) = create_dir(rustlings_path) { if e.kind() == ErrorKind::AlreadyExists { - bail!( - "A directory with the name `rustlings` already exists in the current directory. -You probably already initialized Rustlings. -Run `cd rustlings` -Then run `rustlings` again" - ); + bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); } return Err(e.into()); } @@ -96,3 +82,21 @@ Then run `rustlings` again" Ok(()) } + +const GITIGNORE: &[u8] = b"/target +/.rustlings-state.json"; + +const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; + +const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str = + "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist +in the current directory. It looks like Rustlings was already initialized here. +Run `rustlings` for instructions on getting started with the exercises. + +If you didn't already initialize Rustlings, please initialize it in another directory."; + +const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = + "A directory with the name `rustlings` already exists in the current directory. +You probably already initialized Rustlings. +Run `cd rustlings` +Then run `rustlings` again"; diff --git a/src/main.rs b/src/main.rs index 7bc10ac8..fdbb710c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use clap::{Parser, Subcommand}; use std::{path::Path, process::exit}; mod app_state; -mod consts; mod embedded; mod exercise; mod init; @@ -14,7 +13,6 @@ mod watch; use self::{ app_state::AppState, - consts::WELCOME, exercise::InfoFile, init::init, list::list, @@ -54,11 +52,7 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); - which::which("cargo").context( - "Failed to find `cargo`. -Did you already install Rust? -Try running `cargo --version` to diagnose the problem.", - )?; + which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; let mut info_file = InfoFile::parse()?; info_file.exercises.shrink_to_fit(); @@ -66,20 +60,11 @@ Try running `cargo --version` to diagnose the problem.", if matches!(args.command, Some(Subcommands::Init)) { init(&exercises).context("Initialization failed")?; - println!( - "\nDone initialization!\n -Run `cd rustlings` to go into the generated directory. -Then run `rustlings` for further instructions on getting started." - ); + + println!("{POST_INIT_MSG}"); return Ok(()); } else if !Path::new("exercises").is_dir() { - println!( - " -{WELCOME} - -The `exercises` directory wasn't found in the current directory. -If you are just starting with Rustlings, run the command `rustlings init` to initialize it." - ); + println!("{PRE_INIT_MSG}"); exit(1); } @@ -118,3 +103,45 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini Ok(()) } + +const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; + +const PRE_INIT_MSG: &str = r" + welcome to... + _ _ _ + _ __ _ _ ___| |_| (_)_ __ __ _ ___ + | '__| | | / __| __| | | '_ \ / _` / __| + | | | |_| \__ \ |_| | | | | | (_| \__ \ + |_| \__,_|___/\__|_|_|_| |_|\__, |___/ + |___/ + +The `exercises` directory wasn't found in the current directory. +If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; + +const POST_INIT_MSG: &str = " +Done initialization! + +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` for further instructions on getting started."; + +const FENISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the Fe-nish line! | ++-------------------------- ------------------------+ + \\/\x1b[31m + ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ + ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ + ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ + ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ + ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m"; diff --git a/src/watch.rs b/src/watch.rs index 929275f1..bfa0f88a 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -89,10 +89,12 @@ pub fn watch(app_state: &mut AppState) -> Result { } } - watch_state.into_writer().write_all(b" -We hope you're enjoying learning Rust! -If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. -")?; + watch_state.into_writer().write_all(QUIT_MSG)?; Ok(WatchExit::Shutdown) } + +const QUIT_MSG: &[u8] = b" +We hope you're enjoying learning Rust! +If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. +"; From 6807e63c5f26ee01b60460355ce2c5411c603f16 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 02:45:54 +0200 Subject: [PATCH 118/433] Show done message --- src/watch.rs | 4 ---- src/watch/state.rs | 52 +++++++++++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index bfa0f88a..928fc5fb 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -54,9 +54,7 @@ pub fn watch(app_state: &mut AppState) -> Result { let mut watch_state = WatchState::new(app_state); - // TODO: bool watch_state.run_current_exercise()?; - watch_state.render()?; thread::spawn(move || terminal_event_handler(tx)); @@ -76,9 +74,7 @@ pub fn watch(app_state: &mut AppState) -> Result { watch_state.handle_invalid_cmd(&cmd)?; } WatchEvent::FileChange { exercise_ind } => { - // TODO: bool watch_state.run_exercise_with_ind(exercise_ind)?; - watch_state.render()?; } WatchEvent::NotifyErr(e) => { return Err(Error::from(e).context("Exercise file watcher failed")) diff --git a/src/watch/state.rs b/src/watch/state.rs index a7647d8d..5a5c0ca1 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -13,8 +13,8 @@ pub struct WatchState<'a> { app_state: &'a mut AppState, stdout: Option>, stderr: Option>, - message: Option, - hint_displayed: bool, + show_hint: bool, + show_done: bool, } impl<'a> WatchState<'a> { @@ -26,8 +26,8 @@ impl<'a> WatchState<'a> { app_state, stdout: None, stderr: None, - message: None, - hint_displayed: false, + show_hint: false, + show_done: false, } } @@ -36,29 +36,32 @@ impl<'a> WatchState<'a> { self.writer } - pub fn run_current_exercise(&mut self) -> Result { + pub fn run_current_exercise(&mut self) -> Result<()> { + self.show_hint = false; + let output = self.app_state.current_exercise().run()?; self.stdout = Some(output.stdout); - if !output.status.success() { + if output.status.success() { + self.stderr = None; + self.show_done = true; + } else { self.stderr = Some(output.stderr); - return Ok(false); + self.show_done = false; } - self.stderr = None; - - Ok(true) + self.render() } - pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result { + pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> { self.app_state.set_current_exercise_ind(exercise_ind)?; self.run_current_exercise() } fn show_prompt(&mut self) -> io::Result<()> { - self.writer.write_all(b"\n\n")?; + self.writer.write_all(b"\n")?; - if !self.hint_displayed { + if !self.show_hint { self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; } @@ -84,20 +87,26 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; } - if let Some(message) = &self.message { - self.writer.write_all(message.as_bytes())?; - } - self.writer.write_all(b"\n")?; - if self.hint_displayed { + if self.show_hint { self.writer - .write_fmt(format_args!("\n{}\n", "Hint".bold().cyan().underlined()))?; + .write_fmt(format_args!("{}\n", "Hint".bold().cyan().underlined()))?; self.writer .write_all(self.app_state.current_exercise().hint.as_bytes())?; self.writer.write_all(b"\n\n")?; } + if self.show_done { + self.writer.write_fmt(format_args!( + "{}\n\n", + "Exercise done ✓ +When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀" + .bold() + .green(), + ))?; + } + let line_width = size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), @@ -108,7 +117,7 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"Current exercise: ")?; self.writer.write_fmt(format_args!( - "{}", + "{}\n", self.app_state .current_exercise() .path @@ -122,7 +131,7 @@ impl<'a> WatchState<'a> { } pub fn show_hint(&mut self) -> Result<()> { - self.hint_displayed = true; + self.show_hint = true; self.render() } @@ -133,6 +142,7 @@ impl<'a> WatchState<'a> { self.writer .write_all(b" (confusing input can occur after resizing the terminal)")?; } + self.writer.write_all(b"\n")?; self.show_prompt() } } From 98c5088a39439389a4e198839b47819bfa1b1712 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 14:52:50 +0200 Subject: [PATCH 119/433] Update deps --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5ad8c9d..6c646614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "anstream" From a534de0312ff47d5e87b3bf60d508bdaafb98fbc Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 15:27:29 +0200 Subject: [PATCH 120/433] Implement going to the next exercise --- src/watch.rs | 11 +++++++---- src/watch/state.rs | 23 ++++++++++++++++++++++- src/watch/terminal_event.rs | 2 ++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 928fc5fb..357b5c71 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -26,9 +26,9 @@ use self::{ enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, + TerminalResize, NotifyErr(notify::Error), TerminalEventErr(io::Error), - TerminalResize, } /// Returned by the watch mode to indicate what to do afterwards. @@ -60,15 +60,15 @@ pub fn watch(app_state: &mut AppState) -> Result { while let Ok(event) = rx.recv() { match event { + WatchEvent::Input(InputEvent::Next) => { + watch_state.next_exercise()?; + } WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; } WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } - WatchEvent::TerminalResize => { - watch_state.render()?; - } WatchEvent::Input(InputEvent::Quit) => break, WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { watch_state.handle_invalid_cmd(&cmd)?; @@ -76,6 +76,9 @@ pub fn watch(app_state: &mut AppState) -> Result { WatchEvent::FileChange { exercise_ind } => { watch_state.run_exercise_with_ind(exercise_ind)?; } + WatchEvent::TerminalResize => { + watch_state.render()?; + } WatchEvent::NotifyErr(e) => { return Err(Error::from(e).context("Exercise file watcher failed")) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 5a5c0ca1..462633d1 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -6,7 +6,10 @@ use crossterm::{ }; use std::io::{self, StdoutLock, Write}; -use crate::{app_state::AppState, progress_bar::progress_bar}; +use crate::{ + app_state::{AppState, ExercisesProgress}, + progress_bar::progress_bar, +}; pub struct WatchState<'a> { writer: StdoutLock<'a>, @@ -58,9 +61,27 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } + pub fn next_exercise(&mut self) -> Result<()> { + if !self.show_done { + self.writer + .write_all(b"The current exercise isn't done yet\n")?; + self.show_prompt()?; + return Ok(()); + } + + match self.app_state.done_current_exercise()? { + ExercisesProgress::AllDone => todo!(), + ExercisesProgress::Pending => self.run_current_exercise(), + } + } + fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n")?; + if self.show_done { + self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; + } + if !self.show_hint { self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index faca8a2b..7f7ebe06 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,6 +4,7 @@ use std::sync::mpsc::Sender; use super::WatchEvent; pub enum InputEvent { + Next, Hint, List, Quit, @@ -38,6 +39,7 @@ pub fn terminal_event_handler(tx: Sender) { match key.code { KeyCode::Enter => { let input_event = match input.trim() { + "n" | "next" => InputEvent::Next, "h" | "hint" => InputEvent::Hint, "l" | "list" => break InputEvent::List, "q" | "quit" => break InputEvent::Quit, From d5a6dee1b329f68d00bee61c6b6c7a0adbf8bab5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 18:57:04 +0200 Subject: [PATCH 121/433] Handle the case when all exercises are done --- src/app_state.rs | 52 +++++++++++++++++++++++++++++++++++++++++----- src/run.rs | 24 +++++++++------------ src/watch.rs | 17 ++++++++------- src/watch/state.rs | 34 +++++++++++++++--------------- 4 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 4a0912e4..b1440e8a 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,8 +1,16 @@ use anyhow::{bail, Context, Result}; +use crossterm::{ + style::Stylize, + terminal::{Clear, ClearType}, + ExecutableCommand, +}; use serde::{Deserialize, Serialize}; -use std::fs; +use std::{ + fs, + io::{StdoutLock, Write}, +}; -use crate::exercise::Exercise; +use crate::{exercise::Exercise, FENISH_LINE}; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -143,7 +151,7 @@ impl AppState { Ok(()) } - fn next_exercise_ind(&self) -> Option { + fn next_pending_exercise_ind(&self) -> Option { let current_ind = self.state_file.current_exercise_ind; if current_ind == self.state_file.progress.len() - 1 { @@ -167,14 +175,41 @@ impl AppState { } } - pub fn done_current_exercise(&mut self) -> Result { + pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; if !*done { *done = true; self.n_done += 1; } - let Some(ind) = self.next_exercise_ind() else { + let Some(ind) = self.next_pending_exercise_ind() else { + writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + + for (exercise_ind, exercise) in self.exercises().iter().enumerate() { + writer.write_fmt(format_args!("Running {exercise} ... "))?; + writer.flush()?; + + if !exercise.run()?.status.success() { + self.state_file.current_exercise_ind = exercise_ind; + self.current_exercise = exercise; + + // No check if the exercise is done before setting it to pending + // because no pending exercise was found. + self.state_file.progress[exercise_ind] = false; + self.n_done -= 1; + + self.state_file.write()?; + + return Ok(ExercisesProgress::Pending); + } + + writer.write_fmt(format_args!("{}\n", "ok".green()))?; + } + + writer.execute(Clear(ClearType::All))?; + writer.write_all(FENISH_LINE.as_bytes())?; + // TODO: Show final message. + return Ok(ExercisesProgress::AllDone); }; @@ -183,3 +218,10 @@ impl AppState { Ok(ExercisesProgress::Pending) } } + +const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" +All exercises seem to be done. +Recompiling and running all exercises to make sure that all of them are actually done. +This might take some minutes. + +"; diff --git a/src/run.rs b/src/run.rs index 18da193b..ea790e9a 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Result}; use crossterm::style::Stylize; -use std::io::{stdout, Write}; +use std::io::{self, Write}; use crate::app_state::{AppState, ExercisesProgress}; @@ -8,28 +8,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let output = exercise.run()?; - { - let mut stdout = stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; - } + let mut stdout = io::stdout().lock(); + stdout.write_all(&output.stdout)?; + stdout.write_all(b"\n")?; + stdout.write_all(&output.stderr)?; + stdout.flush()?; if !output.status.success() { bail!("Ran {exercise} with errors"); } - println!( + stdout.write_fmt(format_args!( "{}{}", "✓ Successfully ran ".green(), exercise.path.to_string_lossy().green(), - ); + ))?; - match app_state.done_current_exercise()? { - ExercisesProgress::AllDone => println!( - "🎉 Congratulations! You have done all the exercises! -🔚 There are no more exercises to do next!" - ), + match app_state.done_current_exercise(&mut stdout)? { + ExercisesProgress::AllDone => (), ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), } diff --git a/src/watch.rs b/src/watch.rs index 357b5c71..beb69b3d 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -15,7 +15,7 @@ mod debounce_event; mod state; mod terminal_event; -use crate::app_state::AppState; +use crate::app_state::{AppState, ExercisesProgress}; use self::{ debounce_event::DebounceEventHandler, @@ -32,6 +32,7 @@ enum WatchEvent { } /// Returned by the watch mode to indicate what to do afterwards. +#[must_use] pub enum WatchExit { /// Exit the program. Shutdown, @@ -60,16 +61,20 @@ pub fn watch(app_state: &mut AppState) -> Result { while let Ok(event) = rx.recv() { match event { - WatchEvent::Input(InputEvent::Next) => { - watch_state.next_exercise()?; - } + WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { + ExercisesProgress::AllDone => break, + ExercisesProgress::Pending => watch_state.run_current_exercise()?, + }, WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; } WatchEvent::Input(InputEvent::List) => { return Ok(WatchExit::List); } - WatchEvent::Input(InputEvent::Quit) => break, + WatchEvent::Input(InputEvent::Quit) => { + watch_state.into_writer().write_all(QUIT_MSG)?; + break; + } WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { watch_state.handle_invalid_cmd(&cmd)?; } @@ -88,8 +93,6 @@ pub fn watch(app_state: &mut AppState) -> Result { } } - watch_state.into_writer().write_all(QUIT_MSG)?; - Ok(WatchExit::Shutdown) } diff --git a/src/watch/state.rs b/src/watch/state.rs index 462633d1..70b6ae48 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -4,7 +4,10 @@ use crossterm::{ terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use std::io::{self, StdoutLock, Write}; +use std::{ + io::{self, StdoutLock, Write}, + process::Output, +}; use crate::{ app_state::{AppState, ExercisesProgress}, @@ -49,6 +52,9 @@ impl<'a> WatchState<'a> { self.stderr = None; self.show_done = true; } else { + self.app_state + .set_pending(self.app_state.current_exercise_ind())?; + self.stderr = Some(output.stderr); self.show_done = false; } @@ -61,18 +67,15 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } - pub fn next_exercise(&mut self) -> Result<()> { + pub fn next_exercise(&mut self) -> Result { if !self.show_done { self.writer .write_all(b"The current exercise isn't done yet\n")?; self.show_prompt()?; - return Ok(()); + return Ok(ExercisesProgress::Pending); } - match self.app_state.done_current_exercise()? { - ExercisesProgress::AllDone => todo!(), - ExercisesProgress::Pending => self.run_current_exercise(), - } + self.app_state.done_current_exercise(&mut self.writer) } fn show_prompt(&mut self) -> io::Result<()> { @@ -93,7 +96,7 @@ impl<'a> WatchState<'a> { } pub fn render(&mut self) -> Result<()> { - // Prevent having the first line shifted after clearing because of the prompt. + // Prevent having the first line shifted. self.writer.write_all(b"\n")?; self.writer.execute(Clear(ClearType::All))?; @@ -111,11 +114,11 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.show_hint { - self.writer - .write_fmt(format_args!("{}\n", "Hint".bold().cyan().underlined()))?; - self.writer - .write_all(self.app_state.current_exercise().hint.as_bytes())?; - self.writer.write_all(b"\n\n")?; + self.writer.write_fmt(format_args!( + "{}\n{}\n\n", + "Hint".bold().cyan().underlined(), + self.app_state.current_exercise().hint, + ))?; } if self.show_done { @@ -134,11 +137,8 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise self.app_state.exercises().len() as u16, line_width, )?; - self.writer.write_all(progress_bar.as_bytes())?; - - self.writer.write_all(b"Current exercise: ")?; self.writer.write_fmt(format_args!( - "{}\n", + "{progress_bar}Current exercise: {}\n", self.app_state .current_exercise() .path From 8bd03093eb314f799d7daafbd3f7dcea9a5ef148 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 18:57:39 +0200 Subject: [PATCH 122/433] Add newline at the end of the generated .gitignore --- src/init.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 44747438..093610ab 100644 --- a/src/init.rs +++ b/src/init.rs @@ -84,7 +84,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> { } const GITIGNORE: &[u8] = b"/target -/.rustlings-state.json"; +/.rustlings-state.json +"; const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; From 44824718b2155268c79d1ce216abc770df94d05d Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 18:58:01 +0200 Subject: [PATCH 123/433] Remove unused import --- src/watch/state.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index 70b6ae48..6a97637b 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -4,10 +4,7 @@ use crossterm::{ terminal::{size, Clear, ClearType}, ExecutableCommand, }; -use std::{ - io::{self, StdoutLock, Write}, - process::Output, -}; +use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, From 9b0eeb815acd550d733a722c0563bfb703bb8513 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 19:07:17 +0200 Subject: [PATCH 124/433] Fix Display for Exercise --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index d28f4dbe..a9dcce34 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -117,7 +117,7 @@ impl Exercise { impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.path.fmt(f) + Display::fmt(&self.path.display(), f) } } From 279ebdc1534d70d838110c16e46dce848a9de956 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 19:16:52 +0200 Subject: [PATCH 125/433] Remove the modifier filter in the list mode --- src/list.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/list.rs b/src/list.rs index 2430ed73..de120eaf 100644 --- a/src/list.rs +++ b/src/list.rs @@ -1,6 +1,6 @@ use anyhow::Result; use crossterm::{ - event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, + event::{self, Event, KeyCode, KeyEventKind}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; @@ -28,16 +28,10 @@ pub fn list(app_state: &mut AppState) -> Result<()> { let key = loop { match event::read()? { - Event::Key(key) => { - if key.modifiers != KeyModifiers::NONE { - continue; - } - - match key.kind { - KeyEventKind::Press | KeyEventKind::Repeat => break key, - KeyEventKind::Release => (), - } - } + Event::Key(key) => match key.kind { + KeyEventKind::Press | KeyEventKind::Repeat => break key, + KeyEventKind::Release => (), + }, // Redraw Event::Resize(_, _) => continue 'outer, // Ignore From 6e827da570278b6ff282f3b5c23e2ab95624117e Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 19:18:16 +0200 Subject: [PATCH 126/433] It doesn't take minutes :P --- src/app_state.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app_state.rs b/src/app_state.rs index b1440e8a..18d9e2ae 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -222,6 +222,5 @@ impl AppState { const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. -This might take some minutes. "; From 06d1089714d77e8619fd0b5c34361eec5312363e Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 19:24:26 +0200 Subject: [PATCH 127/433] Set pending on fail in run mode --- src/run.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/run.rs b/src/run.rs index ea790e9a..ebe4f965 100644 --- a/src/run.rs +++ b/src/run.rs @@ -15,6 +15,8 @@ pub fn run(app_state: &mut AppState) -> Result<()> { stdout.flush()?; if !output.status.success() { + app_state.set_pending(app_state.current_exercise_ind())?; + bail!("Ran {exercise} with errors"); } From ff4c7529846ba13ecb2e90616ff8fd7a9ee87164 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 19:30:29 +0200 Subject: [PATCH 128/433] Print FAILED --- src/app_state.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app_state.rs b/src/app_state.rs index 18d9e2ae..cb7debe1 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -190,6 +190,8 @@ impl AppState { writer.flush()?; if !exercise.run()?.status.success() { + writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; + self.state_file.current_exercise_ind = exercise_ind; self.current_exercise = exercise; From 757723a7e8db5822df3b7ca56012448ca292ce4f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 19:30:36 +0200 Subject: [PATCH 129/433] Add missing newline --- src/run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/run.rs b/src/run.rs index ebe4f965..47485492 100644 --- a/src/run.rs +++ b/src/run.rs @@ -21,7 +21,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { } stdout.write_fmt(format_args!( - "{}{}", + "{}{}\n", "✓ Successfully ran ".green(), exercise.path.to_string_lossy().green(), ))?; From 24539666afb0e8c80fbccbca7ad212ba8fbd1189 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 12 Apr 2024 20:06:56 +0200 Subject: [PATCH 130/433] Show the final message --- info.toml | 3 ++- src/app_state.rs | 29 ++++++++++++++++++----------- src/exercise.rs | 1 - src/main.rs | 6 ++++-- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/info.toml b/info.toml index d35b5702..b6b68008 100644 --- a/info.toml +++ b/info.toml @@ -20,7 +20,8 @@ started, here's a couple of notes about how Rustlings operates: and sometimes, other learners do too so you can help each other out! Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the `rustlings` directory!""" +Make sure to have your editor open in the `rustlings` directory! +""" final_message = """We hope you enjoyed learning about the various aspects of Rust! If you noticed any issues, please don't hesitate to report them to our repo. diff --git a/src/app_state.rs b/src/app_state.rs index cb7debe1..2ea3db42 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -51,24 +51,29 @@ impl StateFile { } } -pub struct AppState { - state_file: StateFile, - exercises: &'static [Exercise], - n_done: u16, - current_exercise: &'static Exercise, -} - #[must_use] pub enum ExercisesProgress { AllDone, Pending, } +pub struct AppState { + state_file: StateFile, + exercises: &'static [Exercise], + n_done: u16, + current_exercise: &'static Exercise, + final_message: &'static str, +} + impl AppState { - pub fn new(exercises: Vec) -> Self { - // Leaking for sending the exercises to the debounce event handler. - // Leaking is not a problem since the exercises' slice is used until the end of the program. + pub fn new(mut exercises: Vec, mut final_message: String) -> Self { + // Leaking especially for sending the exercises to the debounce event handler. + // Leaking is not a problem because the `AppState` instance lives until + // the end of the program. + exercises.shrink_to_fit(); let exercises = exercises.leak(); + final_message.shrink_to_fit(); + let final_message = final_message.leak(); let state_file = StateFile::read_or_default(exercises); let n_done = state_file @@ -82,6 +87,7 @@ impl AppState { exercises, n_done, current_exercise, + final_message, } } @@ -210,7 +216,8 @@ impl AppState { writer.execute(Clear(ClearType::All))?; writer.write_all(FENISH_LINE.as_bytes())?; - // TODO: Show final message. + writer.write_all(self.final_message.as_bytes())?; + writer.write_all(b"\n")?; return Ok(ExercisesProgress::AllDone); }; diff --git a/src/exercise.rs b/src/exercise.rs index a9dcce34..a29b83aa 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -26,7 +26,6 @@ pub enum Mode { pub struct InfoFile { // TODO pub welcome_message: Option, - // TODO pub final_message: Option, pub exercises: Vec, } diff --git a/src/main.rs b/src/main.rs index fdbb710c..cdfa21f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,7 +68,7 @@ fn main() -> Result<()> { exit(1); } - let mut app_state = AppState::new(exercises); + let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default()); match args.command { None => loop { @@ -144,4 +144,6 @@ const FENISH_LINE: &str = "+---------------------------------------------------- ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m"; + ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m + +"; From 2a26dfcb005d2a9ee24e920462b37dfb6d235c32 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 13 Apr 2024 15:30:35 +0200 Subject: [PATCH 131/433] Remove unused ContextLine --- src/exercise.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index a29b83aa..6aa3b82e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -63,17 +63,6 @@ pub struct Exercise { pub hint: String, } -// The context information of a pending exercise. -#[derive(PartialEq, Eq, Debug)] -pub struct ContextLine { - // The source code line - pub line: String, - // The line number - pub number: usize, - // Whether this is important and should be highlighted - pub important: bool, -} - impl Exercise { fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { let mut cmd = Command::new("cargo"); From 5c0073a9485c4226e58b657cb49628919a28a942 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 01:15:43 +0200 Subject: [PATCH 132/433] Tolerate changes in the state file --- Cargo.lock | 1 + Cargo.toml | 1 + exercises/00_intro/intro1.rs | 1 - info.toml | 272 +++++++++--------- src/app_state.rs | 205 +++++++------ src/app_state/state_file.rs | 112 ++++++++ src/exercise.rs | 72 +---- src/info_file.rs | 81 ++++++ src/init.rs | 23 +- src/list.rs | 11 +- src/list/state.rs | 35 +-- src/main.rs | 40 ++- src/run.rs | 2 +- src/watch.rs | 15 +- .../{debounce_event.rs => notify_event.rs} | 10 +- 15 files changed, 513 insertions(+), 368 deletions(-) create mode 100644 src/app_state/state_file.rs create mode 100644 src/info_file.rs rename src/watch/{debounce_event.rs => notify_event.rs} (84%) diff --git a/Cargo.lock b/Cargo.lock index 6c646614..dbf1923e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,7 @@ dependencies = [ "assert_cmd", "clap", "crossterm", + "hashbrown", "notify-debouncer-mini", "predicates", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index 285e7df6..14ae9a14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ edition.workspace = true anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" +hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" rustlings-macros = { path = "rustlings-macros" } diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index e4e0444a..170d1958 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,6 +1,5 @@ // intro1.rs // -// TODO: Update comment // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel // ready for the next exercise, remove the `I AM NOT DONE` comment below. diff --git a/info.toml b/info.toml index b6b68008..fa90ad70 100644 --- a/info.toml +++ b/info.toml @@ -33,10 +33,11 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md # INTRO +# TODO: Update exercise [[exercises]] name = "intro1" -path = "exercises/00_intro/intro1.rs" -mode = "compile" +dir = "00_intro" +mode = "run" # TODO: Fix hint hint = """ Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file @@ -44,8 +45,8 @@ to move on to the next exercise.""" [[exercises]] name = "intro2" -path = "exercises/00_intro/intro2.rs" -mode = "compile" +dir = "00_intro" +mode = "run" hint = """ The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" @@ -53,16 +54,16 @@ The compiler is informing us that we've got the name of the print macro wrong, a [[exercises]] name = "variables1" -path = "exercises/01_variables/variables1.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ The declaration in the first line in the main function is missing a keyword that is needed in Rust to create a new variable binding.""" [[exercises]] name = "variables2" -path = "exercises/01_variables/variables2.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ The compiler message is saying that Rust cannot infer the type that the variable binding `x` has with what is given here. @@ -80,8 +81,8 @@ What if `x` is the same type as `10`? What if it's a different type?""" [[exercises]] name = "variables3" -path = "exercises/01_variables/variables3.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ Oops! In this exercise, we have a variable binding that we've created on in the first line in the `main` function, and we're trying to use it in the next line, @@ -94,8 +95,8 @@ programming language -- thankfully the Rust compiler has caught this for us!""" [[exercises]] name = "variables4" -path = "exercises/01_variables/variables4.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ In Rust, variable bindings are immutable by default. But here we're trying to reassign a different value to `x`! There's a keyword we can use to make @@ -103,8 +104,8 @@ a variable binding mutable instead.""" [[exercises]] name = "variables5" -path = "exercises/01_variables/variables5.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ In `variables4` we already learned how to make an immutable variable mutable using a special keyword. Unfortunately this doesn't help us much in this @@ -121,8 +122,8 @@ Try to solve this exercise afterwards using this technique.""" [[exercises]] name = "variables6" -path = "exercises/01_variables/variables6.rs" -mode = "compile" +dir = "01_variables" +mode = "run" hint = """ We know about variables and mutability, but there is another important type of variable available: constants. @@ -141,8 +142,8 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants [[exercises]] name = "functions1" -path = "exercises/02_functions/functions1.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ This main function is calling a function that it expects to exist, but the function doesn't exist. It expects this function to have the name `call_me`. @@ -151,24 +152,24 @@ Sounds a lot like `main`, doesn't it?""" [[exercises]] name = "functions2" -path = "exercises/02_functions/functions2.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ Rust requires that all parts of a function's signature have type annotations, but `call_me` is missing the type annotation of `num`.""" [[exercises]] name = "functions3" -path = "exercises/02_functions/functions3.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ This time, the function *declaration* is okay, but there's something wrong with the place where we're calling the function.""" [[exercises]] name = "functions4" -path = "exercises/02_functions/functions4.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ The error message points to the function `sale_price` and says it expects a type after the `->`. This is where the function's return type should be -- take a @@ -179,8 +180,8 @@ for the inputs of the functions here, since the original prices shouldn't be neg [[exercises]] name = "functions5" -path = "exercises/02_functions/functions5.rs" -mode = "compile" +dir = "02_functions" +mode = "run" hint = """ This is a really common error that can be fixed by removing one character. It happens because Rust distinguishes between expressions and statements: @@ -198,7 +199,7 @@ They are not the same. There are two solutions: [[exercises]] name = "if1" -path = "exercises/03_if/if1.rs" +dir = "03_if" mode = "test" hint = """ It's possible to do this in one line if you would like! @@ -214,7 +215,7 @@ Remember in Rust that: [[exercises]] name = "if2" -path = "exercises/03_if/if2.rs" +dir = "03_if" mode = "test" hint = """ For that first compiler error, it's important in Rust that each conditional @@ -223,7 +224,7 @@ conditions checking different input values.""" [[exercises]] name = "if3" -path = "exercises/03_if/if3.rs" +dir = "03_if" mode = "test" hint = """ In Rust, every arm of an `if` expression has to return the same type of value. @@ -233,7 +234,6 @@ Make sure the type is consistent across all arms.""" [[exercises]] name = "quiz1" -path = "exercises/quiz1.rs" mode = "test" hint = "No hints this time ;)" @@ -241,20 +241,20 @@ hint = "No hints this time ;)" [[exercises]] name = "primitive_types1" -path = "exercises/04_primitive_types/primitive_types1.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = "No hints this time ;)" [[exercises]] name = "primitive_types2" -path = "exercises/04_primitive_types/primitive_types2.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = "No hints this time ;)" [[exercises]] name = "primitive_types3" -path = "exercises/04_primitive_types/primitive_types3.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = """ There's a shorthand to initialize Arrays with a certain size that does not require you to type in 100 items (but you certainly can if you want!). @@ -269,7 +269,7 @@ for `a.len() >= 100`?""" [[exercises]] name = "primitive_types4" -path = "exercises/04_primitive_types/primitive_types4.rs" +dir = "04_primitive_types" mode = "test" hint = """ Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section @@ -284,8 +284,8 @@ https://doc.rust-lang.org/nomicon/coercions.html""" [[exercises]] name = "primitive_types5" -path = "exercises/04_primitive_types/primitive_types5.rs" -mode = "compile" +dir = "04_primitive_types" +mode = "run" hint = """ Take a look at the 'Data Types -> The Tuple Type' section of the book: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type @@ -297,7 +297,7 @@ of the tuple. You can do it!!""" [[exercises]] name = "primitive_types6" -path = "exercises/04_primitive_types/primitive_types6.rs" +dir = "04_primitive_types" mode = "test" hint = """ While you could use a destructuring `let` for the tuple here, try @@ -310,7 +310,7 @@ Now you have another tool in your toolbox!""" [[exercises]] name = "vecs1" -path = "exercises/05_vecs/vecs1.rs" +dir = "05_vecs" mode = "test" hint = """ In Rust, there are two ways to define a Vector. @@ -325,7 +325,7 @@ of the Rust book to learn more. [[exercises]] name = "vecs2" -path = "exercises/05_vecs/vecs2.rs" +dir = "05_vecs" mode = "test" hint = """ In the first function we are looping over the Vector and getting a reference to @@ -348,7 +348,7 @@ What do you think is the more commonly used pattern under Rust developers? [[exercises]] name = "move_semantics1" -path = "exercises/06_move_semantics/move_semantics1.rs" +dir = "06_move_semantics" mode = "test" hint = """ So you've got the "cannot borrow immutable local variable `vec` as mutable" @@ -362,7 +362,7 @@ happens!""" [[exercises]] name = "move_semantics2" -path = "exercises/06_move_semantics/move_semantics2.rs" +dir = "06_move_semantics" mode = "test" hint = """ When running this exercise for the first time, you'll notice an error about @@ -383,7 +383,7 @@ try them all: [[exercises]] name = "move_semantics3" -path = "exercises/06_move_semantics/move_semantics3.rs" +dir = "06_move_semantics" mode = "test" hint = """ The difference between this one and the previous ones is that the first line @@ -393,7 +393,7 @@ an existing binding to be a mutable binding instead of an immutable one :)""" [[exercises]] name = "move_semantics4" -path = "exercises/06_move_semantics/move_semantics4.rs" +dir = "06_move_semantics" mode = "test" hint = """ Stop reading whenever you feel like you have enough direction :) Or try @@ -407,7 +407,7 @@ So the end goal is to: [[exercises]] name = "move_semantics5" -path = "exercises/06_move_semantics/move_semantics5.rs" +dir = "06_move_semantics" mode = "test" hint = """ Carefully reason about the range in which each mutable reference is in @@ -419,8 +419,8 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref [[exercises]] name = "move_semantics6" -path = "exercises/06_move_semantics/move_semantics6.rs" -mode = "compile" +dir = "06_move_semantics" +mode = "run" hint = """ To find the answer, you can consult the book section "References and Borrowing": https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html @@ -440,7 +440,7 @@ Another hint: it has to do with the `&` character.""" [[exercises]] name = "structs1" -path = "exercises/07_structs/structs1.rs" +dir = "07_structs" mode = "test" hint = """ Rust has more than one type of struct. Three actually, all variants are used to @@ -460,7 +460,7 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" [[exercises]] name = "structs2" -path = "exercises/07_structs/structs2.rs" +dir = "07_structs" mode = "test" hint = """ Creating instances of structs is easy, all you need to do is assign some values @@ -472,7 +472,7 @@ https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-ins [[exercises]] name = "structs3" -path = "exercises/07_structs/structs3.rs" +dir = "07_structs" mode = "test" hint = """ For `is_international`: What makes a package international? Seems related to @@ -488,21 +488,21 @@ https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" [[exercises]] name = "enums1" -path = "exercises/08_enums/enums1.rs" -mode = "compile" +dir = "08_enums" +mode = "run" hint = "No hints this time ;)" [[exercises]] name = "enums2" -path = "exercises/08_enums/enums2.rs" -mode = "compile" +dir = "08_enums" +mode = "run" hint = """ You can create enumerations that have different variants with different types such as no data, anonymous structs, a single string, tuples, ...etc""" [[exercises]] name = "enums3" -path = "exercises/08_enums/enums3.rs" +dir = "08_enums" mode = "test" hint = """ As a first step, you can define enums to compile this code without errors. @@ -516,8 +516,8 @@ to get value in the variant.""" [[exercises]] name = "strings1" -path = "exercises/09_strings/strings1.rs" -mode = "compile" +dir = "09_strings" +mode = "run" hint = """ The `current_favorite_color` function is currently returning a string slice with the `'static` lifetime. We know this because the data of the string lives @@ -530,8 +530,8 @@ another way that uses the `From` trait.""" [[exercises]] name = "strings2" -path = "exercises/09_strings/strings2.rs" -mode = "compile" +dir = "09_strings" +mode = "run" hint = """ Yes, it would be really easy to fix this by just changing the value bound to `word` to be a string slice instead of a `String`, wouldn't it?? There is a way @@ -545,7 +545,7 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion [[exercises]] name = "strings3" -path = "exercises/09_strings/strings3.rs" +dir = "09_strings" mode = "test" hint = """ There's tons of useful standard library functions for strings. Let's try and use some of them: @@ -556,16 +556,16 @@ the string slice into an owned string, which you can then freely extend.""" [[exercises]] name = "strings4" -path = "exercises/09_strings/strings4.rs" -mode = "compile" +dir = "09_strings" +mode = "run" hint = "No hints this time ;)" # MODULES [[exercises]] name = "modules1" -path = "exercises/10_modules/modules1.rs" -mode = "compile" +dir = "10_modules" +mode = "run" hint = """ Everything is private in Rust by default-- but there's a keyword we can use to make something public! The compiler error should point to the thing that @@ -573,8 +573,8 @@ needs to be public.""" [[exercises]] name = "modules2" -path = "exercises/10_modules/modules2.rs" -mode = "compile" +dir = "10_modules" +mode = "run" hint = """ The delicious_snacks module is trying to present an external interface that is different than its internal structure (the `fruits` and `veggies` modules and @@ -585,8 +585,8 @@ Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-w [[exercises]] name = "modules3" -path = "exercises/10_modules/modules3.rs" -mode = "compile" +dir = "10_modules" +mode = "run" hint = """ `UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `use` statement for these two to bring them into scope. You can use nested @@ -596,7 +596,7 @@ paths or the glob operator to bring these two in using only one line.""" [[exercises]] name = "hashmaps1" -path = "exercises/11_hashmaps/hashmaps1.rs" +dir = "11_hashmaps" mode = "test" hint = """ Hint 1: Take a look at the return type of the function to figure out @@ -608,7 +608,7 @@ Hint 2: Number of fruits should be at least 5. And you have to put [[exercises]] name = "hashmaps2" -path = "exercises/11_hashmaps/hashmaps2.rs" +dir = "11_hashmaps" mode = "test" hint = """ Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. @@ -617,7 +617,7 @@ Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only- [[exercises]] name = "hashmaps3" -path = "exercises/11_hashmaps/hashmaps3.rs" +dir = "11_hashmaps" mode = "test" hint = """ Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert @@ -635,7 +635,6 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v [[exercises]] name = "quiz2" -path = "exercises/quiz2.rs" mode = "test" hint = "No hints this time ;)" @@ -643,7 +642,7 @@ hint = "No hints this time ;)" [[exercises]] name = "options1" -path = "exercises/12_options/options1.rs" +dir = "12_options" mode = "test" hint = """ Options can have a `Some` value, with an inner value, or a `None` value, @@ -655,7 +654,7 @@ it doesn't panic in your face later?""" [[exercises]] name = "options2" -path = "exercises/12_options/options2.rs" +dir = "12_options" mode = "test" hint = """ Check out: @@ -672,8 +671,8 @@ Also see `Option::flatten` [[exercises]] name = "options3" -path = "exercises/12_options/options3.rs" -mode = "compile" +dir = "12_options" +mode = "run" hint = """ The compiler says a partial move happened in the `match` statement. How can this be avoided? The compiler shows the correction needed. @@ -685,7 +684,7 @@ https://doc.rust-lang.org/std/keyword.ref.html""" [[exercises]] name = "errors1" -path = "exercises/13_error_handling/errors1.rs" +dir = "13_error_handling" mode = "test" hint = """ `Ok` and `Err` are the two variants of `Result`, so what the tests are saying @@ -701,7 +700,7 @@ To make this change, you'll need to: [[exercises]] name = "errors2" -path = "exercises/13_error_handling/errors2.rs" +dir = "13_error_handling" mode = "test" hint = """ One way to handle this is using a `match` statement on @@ -717,8 +716,8 @@ and give it a try!""" [[exercises]] name = "errors3" -path = "exercises/13_error_handling/errors3.rs" -mode = "compile" +dir = "13_error_handling" +mode = "run" hint = """ If other functions can return a `Result`, why shouldn't `main`? It's a fairly common convention to return something like `Result<(), ErrorType>` from your @@ -729,7 +728,7 @@ positive results.""" [[exercises]] name = "errors4" -path = "exercises/13_error_handling/errors4.rs" +dir = "13_error_handling" mode = "test" hint = """ `PositiveNonzeroInteger::new` is always creating a new instance and returning @@ -741,8 +740,8 @@ everything is... okay :)""" [[exercises]] name = "errors5" -path = "exercises/13_error_handling/errors5.rs" -mode = "compile" +dir = "13_error_handling" +mode = "run" hint = """ There are two different possible `Result` types produced within `main()`, which are propagated using `?` operators. How do we declare a return type from @@ -765,7 +764,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen [[exercises]] name = "errors6" -path = "exercises/13_error_handling/errors6.rs" +dir = "13_error_handling" mode = "test" hint = """ This exercise uses a completed version of `PositiveNonzeroInteger` from @@ -787,8 +786,8 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" [[exercises]] name = "generics1" -path = "exercises/14_generics/generics1.rs" -mode = "compile" +dir = "14_generics" +mode = "run" hint = """ Vectors in Rust make use of generics to create dynamically sized arrays of any type. @@ -797,7 +796,7 @@ You need to tell the compiler what type we are pushing onto this vector.""" [[exercises]] name = "generics2" -path = "exercises/14_generics/generics2.rs" +dir = "14_generics" mode = "test" hint = """ Currently we are wrapping only values of type `u32`. @@ -811,7 +810,7 @@ If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html [[exercises]] name = "traits1" -path = "exercises/15_traits/traits1.rs" +dir = "15_traits" mode = "test" hint = """ A discussion about Traits in Rust can be found at: @@ -820,7 +819,7 @@ https://doc.rust-lang.org/book/ch10-02-traits.html [[exercises]] name = "traits2" -path = "exercises/15_traits/traits2.rs" +dir = "15_traits" mode = "test" hint = """ Notice how the trait takes ownership of `self`, and returns `Self`. @@ -833,7 +832,7 @@ the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" [[exercises]] name = "traits3" -path = "exercises/15_traits/traits3.rs" +dir = "15_traits" mode = "test" hint = """ Traits can have a default implementation for functions. Structs that implement @@ -845,7 +844,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#def [[exercises]] name = "traits4" -path = "exercises/15_traits/traits4.rs" +dir = "15_traits" mode = "test" hint = """ Instead of using concrete types as parameters you can use traits. Try replacing @@ -856,8 +855,8 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#tra [[exercises]] name = "traits5" -path = "exercises/15_traits/traits5.rs" -mode = "compile" +dir = "15_traits" +mode = "run" hint = """ To ensure a parameter implements multiple traits use the '+ syntax'. Try replacing the '??' with 'impl <> + <>'. @@ -869,7 +868,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe [[exercises]] name = "quiz3" -path = "exercises/quiz3.rs" mode = "test" hint = """ To find the best solution to this challenge you're going to need to think back @@ -881,16 +879,16 @@ You may also need this: `use std::fmt::Display;`.""" [[exercises]] name = "lifetimes1" -path = "exercises/16_lifetimes/lifetimes1.rs" -mode = "compile" +dir = "16_lifetimes" +mode = "run" hint = """ Let the compiler guide you. Also take a look at the book if you need help: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" [[exercises]] name = "lifetimes2" -path = "exercises/16_lifetimes/lifetimes2.rs" -mode = "compile" +dir = "16_lifetimes" +mode = "run" hint = """ Remember that the generic lifetime `'a` will get the concrete lifetime that is equal to the smaller of the lifetimes of `x` and `y`. @@ -903,8 +901,8 @@ inner block: [[exercises]] name = "lifetimes3" -path = "exercises/16_lifetimes/lifetimes3.rs" -mode = "compile" +dir = "16_lifetimes" +mode = "run" hint = """ If you use a lifetime annotation in a struct's fields, where else does it need to be added?""" @@ -913,7 +911,7 @@ to be added?""" [[exercises]] name = "tests1" -path = "exercises/17_tests/tests1.rs" +dir = "17_tests" mode = "test" hint = """ You don't even need to write any code to test -- you can just test values and @@ -928,7 +926,7 @@ ones pass, and which ones fail :)""" [[exercises]] name = "tests2" -path = "exercises/17_tests/tests2.rs" +dir = "17_tests" mode = "test" hint = """ Like the previous exercise, you don't need to write any code to get this test @@ -941,7 +939,7 @@ argument comes first and which comes second!""" [[exercises]] name = "tests3" -path = "exercises/17_tests/tests3.rs" +dir = "17_tests" mode = "test" hint = """ You can call a function right where you're passing arguments to `assert!`. So @@ -952,7 +950,7 @@ what you're doing using `!`, like `assert!(!having_fun())`.""" [[exercises]] name = "tests4" -path = "exercises/17_tests/tests4.rs" +dir = "17_tests" mode = "test" hint = """ We expect method `Rectangle::new()` to panic for negative values. @@ -966,7 +964,7 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa [[exercises]] name = "iterators1" -path = "exercises/18_iterators/iterators1.rs" +dir = "18_iterators" mode = "test" hint = """ Step 1: @@ -989,7 +987,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. [[exercises]] name = "iterators2" -path = "exercises/18_iterators/iterators2.rs" +dir = "18_iterators" mode = "test" hint = """ Step 1: @@ -1015,7 +1013,7 @@ powerful and very general. Rust just needs to know the desired type.""" [[exercises]] name = "iterators3" -path = "exercises/18_iterators/iterators3.rs" +dir = "18_iterators" mode = "test" hint = """ The `divide` function needs to return the correct error when even division is @@ -1034,7 +1032,7 @@ powerful! It can make the solution to this exercise infinitely easier.""" [[exercises]] name = "iterators4" -path = "exercises/18_iterators/iterators4.rs" +dir = "18_iterators" mode = "test" hint = """ In an imperative language, you might write a `for` loop that updates a mutable @@ -1046,7 +1044,7 @@ Hint 2: Check out the `fold` and `rfold` methods!""" [[exercises]] name = "iterators5" -path = "exercises/18_iterators/iterators5.rs" +dir = "18_iterators" mode = "test" hint = """ The documentation for the `std::iter::Iterator` trait contains numerous methods @@ -1065,7 +1063,7 @@ a different method that could make your code more compact than using `fold`.""" [[exercises]] name = "box1" -path = "exercises/19_smart_pointers/box1.rs" +dir = "19_smart_pointers" mode = "test" hint = """ Step 1: @@ -1089,7 +1087,7 @@ definition and try other types! [[exercises]] name = "rc1" -path = "exercises/19_smart_pointers/rc1.rs" +dir = "19_smart_pointers" mode = "test" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has @@ -1108,8 +1106,8 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html [[exercises]] name = "arc1" -path = "exercises/19_smart_pointers/arc1.rs" -mode = "compile" +dir = "19_smart_pointers" +mode = "run" hint = """ Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order to avoid creating a copy of `numbers`, you'll need to create `child_numbers` @@ -1126,7 +1124,7 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html [[exercises]] name = "cow1" -path = "exercises/19_smart_pointers/cow1.rs" +dir = "19_smart_pointers" mode = "test" hint = """ If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is @@ -1140,8 +1138,8 @@ on the `Cow` type. [[exercises]] name = "threads1" -path = "exercises/20_threads/threads1.rs" -mode = "compile" +dir = "20_threads" +mode = "run" hint = """ `JoinHandle` is a struct that is returned from a spawned thread: https://doc.rust-lang.org/std/thread/fn.spawn.html @@ -1158,8 +1156,8 @@ https://doc.rust-lang.org/std/thread/struct.JoinHandle.html [[exercises]] name = "threads2" -path = "exercises/20_threads/threads2.rs" -mode = "compile" +dir = "20_threads" +mode = "run" hint = """ `Arc` is an Atomic Reference Counted pointer that allows safe, shared access to **immutable** data. But we want to *change* the number of `jobs_completed` @@ -1180,7 +1178,7 @@ https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-betwee [[exercises]] name = "threads3" -path = "exercises/20_threads/threads3.rs" +dir = "20_threads" mode = "test" hint = """ An alternate way to handle concurrency between threads is to use an `mpsc` @@ -1199,8 +1197,8 @@ See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. [[exercises]] name = "macros1" -path = "exercises/21_macros/macros1.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ When you call a macro, you need to add something special compared to a regular function call. If you're stuck, take a look at what's inside @@ -1208,8 +1206,8 @@ regular function call. If you're stuck, take a look at what's inside [[exercises]] name = "macros2" -path = "exercises/21_macros/macros2.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ Macros don't quite play by the same rules as the rest of Rust, in terms of what's available where. @@ -1219,8 +1217,8 @@ Unlike other things in Rust, the order of "where you define a macro" versus [[exercises]] name = "macros3" -path = "exercises/21_macros/macros3.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ In order to use a macro outside of its module, you need to do something special to the module to lift the macro out into its parent. @@ -1230,8 +1228,8 @@ exported macros, if you've seen any of those around.""" [[exercises]] name = "macros4" -path = "exercises/21_macros/macros4.rs" -mode = "compile" +dir = "21_macros" +mode = "run" hint = """ You only need to add a single character to make this compile. @@ -1247,7 +1245,7 @@ https://veykril.github.io/tlborm/""" [[exercises]] name = "clippy1" -path = "exercises/22_clippy/clippy1.rs" +dir = "22_clippy" mode = "clippy" hint = """ Rust stores the highest precision version of any long or infinite precision @@ -1263,14 +1261,14 @@ appropriate replacement constant from `std::f32::consts`...""" [[exercises]] name = "clippy2" -path = "exercises/22_clippy/clippy2.rs" +dir = "22_clippy" mode = "clippy" hint = """ `for` loops over `Option` values are more clearly expressed as an `if let`""" [[exercises]] name = "clippy3" -path = "exercises/22_clippy/clippy3.rs" +dir = "22_clippy" mode = "clippy" hint = "No hints this time!" @@ -1278,7 +1276,7 @@ hint = "No hints this time!" [[exercises]] name = "using_as" -path = "exercises/23_conversions/using_as.rs" +dir = "23_conversions" mode = "test" hint = """ Use the `as` operator to cast one of the operands in the last line of the @@ -1286,14 +1284,14 @@ Use the `as` operator to cast one of the operands in the last line of the [[exercises]] name = "from_into" -path = "exercises/23_conversions/from_into.rs" +dir = "23_conversions" mode = "test" hint = """ Follow the steps provided right before the `From` implementation""" [[exercises]] name = "from_str" -path = "exercises/23_conversions/from_str.rs" +dir = "23_conversions" mode = "test" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, @@ -1314,7 +1312,7 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen [[exercises]] name = "try_from_into" -path = "exercises/23_conversions/try_from_into.rs" +dir = "23_conversions" mode = "test" hint = """ Follow the steps provided right before the `TryFrom` implementation. @@ -1337,7 +1335,7 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer [[exercises]] name = "as_ref_mut" -path = "exercises/23_conversions/as_ref_mut.rs" +dir = "23_conversions" mode = "test" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/src/app_state.rs b/src/app_state.rs index 2ea3db42..1a051b97 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -4,53 +4,17 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; -use serde::{Deserialize, Serialize}; -use std::{ - fs, - io::{StdoutLock, Write}, -}; +use std::io::{StdoutLock, Write}; -use crate::{exercise::Exercise, FENISH_LINE}; +mod state_file; +use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE}; + +use self::state_file::{write, StateFileDeser}; + +const STATE_FILE_NAME: &str = ".rustlings-state.json"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct StateFile { - current_exercise_ind: usize, - progress: Vec, -} - -impl StateFile { - fn read(exercises: &[Exercise]) -> Option { - let file_content = fs::read(".rustlings-state.json").ok()?; - - let slf: Self = serde_json::de::from_slice(&file_content).ok()?; - - if slf.progress.len() != exercises.len() || slf.current_exercise_ind >= exercises.len() { - return None; - } - - Some(slf) - } - - fn read_or_default(exercises: &[Exercise]) -> Self { - Self::read(exercises).unwrap_or_else(|| Self { - current_exercise_ind: 0, - progress: vec![false; exercises.len()], - }) - } - - fn write(&self) -> Result<()> { - let mut buf = Vec::with_capacity(1024); - serde_json::ser::to_writer(&mut buf, self).context("Failed to serialize the state")?; - fs::write(".rustlings-state.json", buf) - .context("Failed to write the state file `.rustlings-state.json`")?; - - Ok(()) - } -} - #[must_use] pub enum ExercisesProgress { AllDone, @@ -58,52 +22,85 @@ pub enum ExercisesProgress { } pub struct AppState { - state_file: StateFile, - exercises: &'static [Exercise], + current_exercise_ind: usize, + exercises: Vec, n_done: u16, - current_exercise: &'static Exercise, - final_message: &'static str, + welcome_message: String, + final_message: String, } impl AppState { - pub fn new(mut exercises: Vec, mut final_message: String) -> Self { - // Leaking especially for sending the exercises to the debounce event handler. - // Leaking is not a problem because the `AppState` instance lives until - // the end of the program. - exercises.shrink_to_fit(); - let exercises = exercises.leak(); - final_message.shrink_to_fit(); - let final_message = final_message.leak(); + pub fn new(info_file: InfoFile) -> Self { + let mut exercises = info_file + .exercises + .into_iter() + .map(|mut exercise_info| { + // Leaking to be able to borrow in the watch mode `Table`. + // Leaking is not a problem because the `AppState` instance lives until + // the end of the program. + let path = Box::leak(exercise_info.path().into_boxed_path()); - let state_file = StateFile::read_or_default(exercises); - let n_done = state_file - .progress - .iter() - .fold(0, |acc, done| acc + u16::from(*done)); - let current_exercise = &exercises[state_file.current_exercise_ind]; + exercise_info.name.shrink_to_fit(); + let name = exercise_info.name.leak(); + + let hint = exercise_info.hint.trim().to_owned(); + + Exercise { + name, + path, + mode: exercise_info.mode, + hint, + done: false, + } + }) + .collect::>(); + + let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| { + let mut state_file_exercises = + hashbrown::HashMap::with_capacity(state_file.exercises.len()); + + for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() { + state_file_exercises.insert( + exercise_state.name, + (ind == state_file.current_exercise_ind, exercise_state.done), + ); + } + + let mut current_exercise_ind = 0; + let mut n_done = 0; + for (ind, exercise) in exercises.iter_mut().enumerate() { + if let Some((current, done)) = state_file_exercises.get(exercise.name) { + if *done { + exercise.done = true; + n_done += 1; + } + + if *current { + current_exercise_ind = ind; + } + } + } + + (current_exercise_ind, n_done) + }); Self { - state_file, + current_exercise_ind, exercises, n_done, - current_exercise, - final_message, + welcome_message: info_file.welcome_message.unwrap_or_default(), + final_message: info_file.final_message.unwrap_or_default(), } } #[inline] pub fn current_exercise_ind(&self) -> usize { - self.state_file.current_exercise_ind + self.current_exercise_ind } #[inline] - pub fn progress(&self) -> &[bool] { - &self.state_file.progress - } - - #[inline] - pub fn exercises(&self) -> &'static [Exercise] { - self.exercises + pub fn exercises(&self) -> &[Exercise] { + &self.exercises } #[inline] @@ -112,8 +109,8 @@ impl AppState { } #[inline] - pub fn current_exercise(&self) -> &'static Exercise { - self.current_exercise + pub fn current_exercise(&self) -> &Exercise { + &self.exercises[self.current_exercise_ind] } pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { @@ -121,70 +118,61 @@ impl AppState { bail!(BAD_INDEX_ERR); } - self.state_file.current_exercise_ind = ind; - self.current_exercise = &self.exercises[ind]; + self.current_exercise_ind = ind; - self.state_file.write() + write(self) } pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { - let (ind, exercise) = self + // O(N) is fine since this method is used only once until the program exits. + // Building a hashmap would have more overhead. + self.current_exercise_ind = self .exercises .iter() - .enumerate() - .find(|(_, exercise)| exercise.name == name) + .position(|exercise| exercise.name == name) .with_context(|| format!("No exercise found for '{name}'!"))?; - self.state_file.current_exercise_ind = ind; - self.current_exercise = exercise; - - self.state_file.write() + write(self) } pub fn set_pending(&mut self, ind: usize) -> Result<()> { - let done = self - .state_file - .progress - .get_mut(ind) - .context(BAD_INDEX_ERR)?; + let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?; - if *done { - *done = false; + if exercise.done { + exercise.done = false; self.n_done -= 1; - self.state_file.write()?; + write(self)?; } Ok(()) } fn next_pending_exercise_ind(&self) -> Option { - let current_ind = self.state_file.current_exercise_ind; - - if current_ind == self.state_file.progress.len() - 1 { + if self.current_exercise_ind == self.exercises.len() - 1 { // The last exercise is done. // Search for exercises not done from the start. - return self.state_file.progress[..current_ind] + return self.exercises[..self.current_exercise_ind] .iter() - .position(|done| !done); + .position(|exercise| !exercise.done); } // The done exercise isn't the last one. // Search for a pending exercise after the current one and then from the start. - match self.state_file.progress[current_ind + 1..] + match self.exercises[self.current_exercise_ind + 1..] .iter() - .position(|done| !done) + .position(|exercise| !exercise.done) { - Some(ind) => Some(current_ind + 1 + ind), - None => self.state_file.progress[..current_ind] + Some(ind) => Some(self.current_exercise_ind + 1 + ind), + None => self.exercises[..self.current_exercise_ind] .iter() - .position(|done| !done), + .position(|exercise| !exercise.done), } } pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { - let done = &mut self.state_file.progress[self.state_file.current_exercise_ind]; - if !*done { - *done = true; + let exercise = &mut self.exercises[self.current_exercise_ind]; + if !exercise.done { + exercise.done = true; self.n_done += 1; } @@ -198,15 +186,14 @@ impl AppState { if !exercise.run()?.status.success() { writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; - self.state_file.current_exercise_ind = exercise_ind; - self.current_exercise = exercise; + self.current_exercise_ind = exercise_ind; // No check if the exercise is done before setting it to pending // because no pending exercise was found. - self.state_file.progress[exercise_ind] = false; + self.exercises[exercise_ind].done = false; self.n_done -= 1; - self.state_file.write()?; + write(self)?; return Ok(ExercisesProgress::Pending); } diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs new file mode 100644 index 00000000..364a1fa3 --- /dev/null +++ b/src/app_state/state_file.rs @@ -0,0 +1,112 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; + +use crate::exercise::Exercise; + +use super::{AppState, STATE_FILE_NAME}; + +#[derive(Deserialize)] +pub struct ExerciseStateDeser { + pub name: String, + pub done: bool, +} + +#[derive(Serialize)] +struct ExerciseStateSer<'a> { + name: &'a str, + done: bool, +} + +struct ExercisesStateSerializer<'a>(&'a [Exercise]); + +impl<'a> Serialize for ExercisesStateSerializer<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let iter = self.0.iter().map(|exercise| ExerciseStateSer { + name: exercise.name, + done: exercise.done, + }); + + serializer.collect_seq(iter) + } +} + +#[derive(Deserialize)] +pub struct StateFileDeser { + pub current_exercise_ind: usize, + pub exercises: Vec, +} + +#[derive(Serialize)] +struct StateFileSer<'a> { + current_exercise_ind: usize, + exercises: ExercisesStateSerializer<'a>, +} + +impl StateFileDeser { + pub fn read() -> Option { + let file_content = fs::read(STATE_FILE_NAME).ok()?; + serde_json::de::from_slice(&file_content).ok() + } +} + +pub fn write(app_state: &AppState) -> Result<()> { + let content = StateFileSer { + current_exercise_ind: app_state.current_exercise_ind, + exercises: ExercisesStateSerializer(&app_state.exercises), + }; + + let mut buf = Vec::with_capacity(1024); + serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?; + fs::write(STATE_FILE_NAME, buf) + .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use crate::info_file::Mode; + + use super::*; + + #[test] + fn ser_deser_sync() { + let current_exercise_ind = 1; + let exercises = [ + Exercise { + name: "1", + path: Path::new("exercises/1.rs"), + mode: Mode::Run, + hint: String::new(), + done: true, + }, + Exercise { + name: "2", + path: Path::new("exercises/2.rs"), + mode: Mode::Test, + hint: String::new(), + done: false, + }, + ]; + + let ser = StateFileSer { + current_exercise_ind, + exercises: ExercisesStateSerializer(&exercises), + }; + let deser: StateFileDeser = + serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap(); + + assert_eq!(deser.current_exercise_ind, current_exercise_ind); + assert!(deser + .exercises + .iter() + .zip(exercises) + .all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done)); + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 6aa3b82e..c5ece5f5 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,66 +1,25 @@ use anyhow::{Context, Result}; -use serde::Deserialize; use std::{ - fmt::{self, Debug, Display, Formatter}, - fs::{self}, - path::PathBuf, + fmt::{self, Display, Formatter}, + path::Path, process::{Command, Output}, }; -use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; +use crate::{ + embedded::{WriteStrategy, EMBEDDED_FILES}, + info_file::Mode, +}; -// The mode of the exercise. -#[derive(Deserialize, Copy, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Mode { - // The exercise should be compiled as a binary - Compile, - // The exercise should be compiled as a test harness - Test, - // The exercise should be linted with clippy - Clippy, -} - -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] -pub struct InfoFile { - // TODO - pub welcome_message: Option, - pub final_message: Option, - pub exercises: Vec, -} - -impl InfoFile { - pub fn parse() -> Result { - // Read a local `info.toml` if it exists. - // Mainly to let the tests work for now. - let slf: Self = if let Ok(file_content) = fs::read_to_string("info.toml") { - toml_edit::de::from_str(&file_content) - } else { - toml_edit::de::from_str(include_str!("../info.toml")) - } - .context("Failed to parse `info.toml`")?; - - if slf.exercises.is_empty() { - panic!("{NO_EXERCISES_ERR}"); - } - - Ok(slf) - } -} - -// Deserialized from the `info.toml` file. -#[derive(Deserialize)] -#[serde(deny_unknown_fields)] pub struct Exercise { - // Name of the exercise - pub name: String, - // The path to the file containing the exercise's source code - pub path: PathBuf, + // Exercise's unique name + pub name: &'static str, + // Exercise's path + pub path: &'static Path, // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise pub hint: String, + pub done: bool, } impl Exercise { @@ -79,7 +38,7 @@ impl Exercise { .arg("always") .arg("-q") .arg("--bin") - .arg(&self.name) + .arg(self.name) .args(args) .output() .context("Failed to run Cargo") @@ -87,7 +46,7 @@ impl Exercise { pub fn run(&self) -> Result { match self.mode { - Mode::Compile => self.cargo_cmd("run", &[]), + Mode::Run => self.cargo_cmd("run", &[]), Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]), Mode::Clippy => self.cargo_cmd( "clippy", @@ -98,7 +57,7 @@ impl Exercise { pub fn reset(&self) -> Result<()> { EMBEDDED_FILES - .write_exercise_to_disk(&self.path, WriteStrategy::Overwrite) + .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {self}")) } } @@ -108,6 +67,3 @@ impl Display for Exercise { Display::fmt(&self.path.display(), f) } } - -const NO_EXERCISES_ERR: &str = "There are no exercises yet! -If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/info_file.rs b/src/info_file.rs new file mode 100644 index 00000000..dc97b926 --- /dev/null +++ b/src/info_file.rs @@ -0,0 +1,81 @@ +use anyhow::{bail, Context, Error, Result}; +use serde::Deserialize; +use std::{fs, path::PathBuf}; + +// The mode of the exercise. +#[derive(Deserialize, Copy, Clone)] +#[serde(rename_all = "lowercase")] +pub enum Mode { + // The exercise should be compiled as a binary + Run, + // The exercise should be compiled as a test harness + Test, + // The exercise should be linted with clippy + Clippy, +} + +// Deserialized from the `info.toml` file. +#[derive(Deserialize)] +pub struct ExerciseInfo { + // Name of the exercise + pub name: String, + // The exercise's directory inside the `exercises` directory + pub dir: Option, + // The mode of the exercise + pub mode: Mode, + // The hint text associated with the exercise + pub hint: String, +} + +impl ExerciseInfo { + pub fn path(&self) -> PathBuf { + let path = if let Some(dir) = &self.dir { + format!("exercises/{dir}/{}.rs", self.name) + } else { + format!("exercises/{}.rs", self.name) + }; + + PathBuf::from(path) + } +} + +#[derive(Deserialize)] +pub struct InfoFile { + pub welcome_message: Option, + pub final_message: Option, + pub exercises: Vec, +} + +impl InfoFile { + pub fn parse() -> Result { + // Read a local `info.toml` if it exists. + let slf: Self = match fs::read_to_string("info.toml") { + Ok(file_content) => toml_edit::de::from_str(&file_content) + .context("Failed to parse the `info.toml` file")?, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + toml_edit::de::from_str(include_str!("../info.toml")) + .context("Failed to parse the embedded `info.toml` file")? + } + _ => return Err(Error::from(e).context("Failed to read the `info.toml` file")), + }, + }; + + if slf.exercises.is_empty() { + bail!("{NO_EXERCISES_ERR}"); + } + + let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len()); + for exercise in &slf.exercises { + if !names_set.insert(exercise.name.as_str()) { + bail!("Exercise names must all be unique!") + } + } + drop(names_set); + + Ok(slf) + } +} + +const NO_EXERCISES_ERR: &str = "There are no exercises yet! +If you are developing third-party exercises, add at least one exercise before testing."; diff --git a/src/init.rs b/src/init.rs index 093610ab..2badf376 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,17 +6,21 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, exercise::Exercise}; +use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; -fn create_cargo_toml(exercises: &[Exercise]) -> io::Result<()> { +fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); cargo_toml.extend_from_slice(b"bin = [\n"); - for exercise in exercises { + for exercise_info in exercise_infos { cargo_toml.extend_from_slice(b" { name = \""); - cargo_toml.extend_from_slice(exercise.name.as_bytes()); - cargo_toml.extend_from_slice(b"\", path = \""); - cargo_toml.extend_from_slice(exercise.path.to_str().unwrap().as_bytes()); - cargo_toml.extend_from_slice(b"\" },\n"); + cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); + cargo_toml.extend_from_slice(b"\", path = \"exercises/"); + if let Some(dir) = &exercise_info.dir { + cargo_toml.extend_from_slice(dir.as_bytes()); + cargo_toml.extend_from_slice(b"/"); + } + cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); + cargo_toml.extend_from_slice(b".rs\" },\n"); } cargo_toml.extend_from_slice( @@ -54,7 +58,7 @@ fn create_vscode_dir() -> Result<()> { Ok(()) } -pub fn init(exercises: &[Exercise]) -> Result<()> { +pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); } @@ -74,7 +78,8 @@ pub fn init(exercises: &[Exercise]) -> Result<()> { .init_exercises_dir() .context("Failed to initialize the `rustlings/exercises` directory")?; - create_cargo_toml(exercises).context("Failed to create the file `rustlings/Cargo.toml`")?; + create_cargo_toml(exercise_infos) + .context("Failed to create the file `rustlings/Cargo.toml`")?; create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; diff --git a/src/list.rs b/src/list.rs index de120eaf..2bb813d8 100644 --- a/src/list.rs +++ b/src/list.rs @@ -5,7 +5,7 @@ use crossterm::{ ExecutableCommand, }; use ratatui::{backend::CrosstermBackend, Terminal}; -use std::{fmt::Write, io}; +use std::io; mod state; @@ -72,14 +72,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> { ui_state.message.push_str(message); } KeyCode::Char('r') => { - let Some(exercise) = ui_state.reset_selected()? else { - continue; - }; - - ui_state = ui_state.with_updated_rows(); - ui_state - .message - .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; + ui_state = ui_state.with_reset_selected()?; } KeyCode::Char('c') => { ui_state.selected_to_current_exercise()?; diff --git a/src/list/state.rs b/src/list/state.rs index 0dcfe88a..38391a49 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -6,8 +6,9 @@ use ratatui::{ widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, Frame, }; +use std::fmt::Write; -use crate::{app_state::AppState, exercise::Exercise, progress_bar::progress_bar_ratatui}; +use crate::{app_state::AppState, progress_bar::progress_bar_ratatui}; #[derive(Copy, Clone, PartialEq, Eq)] pub enum Filter { @@ -34,10 +35,9 @@ impl<'a> UiState<'a> { .app_state .exercises() .iter() - .zip(self.app_state.progress().iter().copied()) .enumerate() - .filter_map(|(ind, (exercise, done))| { - let exercise_state = if done { + .filter_map(|(ind, exercise)| { + let exercise_state = if exercise.done { if self.filter == Filter::Pending { return None; } @@ -62,7 +62,7 @@ impl<'a> UiState<'a> { Some(Row::new([ next, exercise_state, - Span::raw(&exercise.name), + Span::raw(exercise.name), Span::raw(exercise.path.to_string_lossy()), ])) }); @@ -212,29 +212,30 @@ impl<'a> UiState<'a> { Ok(()) } - pub fn reset_selected(&mut self) -> Result> { + pub fn with_reset_selected(mut self) -> Result { let Some(selected) = self.table_state.selected() else { - return Ok(None); + return Ok(self); }; let (ind, exercise) = self .app_state .exercises() .iter() - .zip(self.app_state.progress()) .enumerate() - .filter_map(|(ind, (exercise, done))| match self.filter { - Filter::Done => done.then_some((ind, exercise)), - Filter::Pending => (!done).then_some((ind, exercise)), + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some((ind, exercise)), + Filter::Pending => (!exercise.done).then_some((ind, exercise)), Filter::None => Some((ind, exercise)), }) .nth(selected) .context("Invalid selection index")?; - self.app_state.set_pending(ind)?; exercise.reset()?; + self.message + .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; + self.app_state.set_pending(ind)?; - Ok(Some(exercise)) + Ok(self.with_updated_rows()) } pub fn selected_to_current_exercise(&mut self) -> Result<()> { @@ -244,12 +245,12 @@ impl<'a> UiState<'a> { let ind = self .app_state - .progress() + .exercises() .iter() .enumerate() - .filter_map(|(ind, done)| match self.filter { - Filter::Done => done.then_some(ind), - Filter::Pending => (!done).then_some(ind), + .filter_map(|(ind, exercise)| match self.filter { + Filter::Done => exercise.done.then_some(ind), + Filter::Pending => (!exercise.done).then_some(ind), Filter::None => Some(ind), }) .nth(selected) diff --git a/src/main.rs b/src/main.rs index cdfa21f6..a96e3230 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::{path::Path, process::exit}; mod app_state; mod embedded; mod exercise; +mod info_file; mod init; mod list; mod progress_bar; @@ -13,7 +14,7 @@ mod watch; use self::{ app_state::AppState, - exercise::InfoFile, + info_file::InfoFile, init::init, list::list, run::run, @@ -54,12 +55,10 @@ fn main() -> Result<()> { which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; - let mut info_file = InfoFile::parse()?; - info_file.exercises.shrink_to_fit(); - let exercises = info_file.exercises; + let info_file = InfoFile::parse()?; if matches!(args.command, Some(Subcommands::Init)) { - init(&exercises).context("Initialization failed")?; + init(&info_file.exercises).context("Initialization failed")?; println!("{POST_INIT_MSG}"); return Ok(()); @@ -68,18 +67,29 @@ fn main() -> Result<()> { exit(1); } - let mut app_state = AppState::new(exercises, info_file.final_message.unwrap_or_default()); + let mut app_state = AppState::new(info_file); match args.command { - None => loop { - match watch(&mut app_state)? { - WatchExit::Shutdown => break, - // It is much easier to exit the watch mode, launch the list mode and then restart - // the watch mode instead of trying to pause the watch threads and correct the - // watch state. - WatchExit::List => list(&mut app_state)?, + None => { + // For the the notify event handler thread. + // Leaking is not a problem because the slice lives until the end of the program. + let exercise_paths = app_state + .exercises() + .iter() + .map(|exercise| exercise.path) + .collect::>() + .leak(); + + loop { + match watch(&mut app_state, exercise_paths)? { + WatchExit::Shutdown => break, + // It is much easier to exit the watch mode, launch the list mode and then restart + // the watch mode instead of trying to pause the watch threads and correct the + // watch state. + WatchExit::List => list(&mut app_state)?, + } } - }, + } // `Init` is handled above. Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { @@ -90,10 +100,10 @@ fn main() -> Result<()> { } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - app_state.set_pending(app_state.current_exercise_ind())?; let exercise = app_state.current_exercise(); exercise.reset()?; println!("The exercise {exercise} has been reset!"); + app_state.set_pending(app_state.current_exercise_ind())?; } Some(Subcommands::Hint { name }) => { app_state.set_current_exercise_by_name(&name)?; diff --git a/src/run.rs b/src/run.rs index 47485492..9c504b53 100644 --- a/src/run.rs +++ b/src/run.rs @@ -17,7 +17,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { if !output.status.success() { app_state.set_pending(app_state.current_exercise_ind())?; - bail!("Ran {exercise} with errors"); + bail!("Ran {} with errors", app_state.current_exercise()); } stdout.write_fmt(format_args!( diff --git a/src/watch.rs b/src/watch.rs index beb69b3d..58e829f3 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -11,14 +11,14 @@ use std::{ time::Duration, }; -mod debounce_event; +mod notify_event; mod state; mod terminal_event; use crate::app_state::{AppState, ExercisesProgress}; use self::{ - debounce_event::DebounceEventHandler, + notify_event::DebounceEventHandler, state::WatchState, terminal_event::{terminal_event_handler, InputEvent}, }; @@ -40,13 +40,16 @@ pub enum WatchExit { List, } -pub fn watch(app_state: &mut AppState) -> Result { +pub fn watch( + app_state: &mut AppState, + exercise_paths: &'static [&'static Path], +) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( Duration::from_secs(1), DebounceEventHandler { tx: tx.clone(), - exercises: app_state.exercises(), + exercise_paths, }, )?; debouncer @@ -85,10 +88,10 @@ pub fn watch(app_state: &mut AppState) -> Result { watch_state.render()?; } WatchEvent::NotifyErr(e) => { - return Err(Error::from(e).context("Exercise file watcher failed")) + return Err(Error::from(e).context("Exercise file watcher failed")); } WatchEvent::TerminalEventErr(e) => { - return Err(Error::from(e).context("Terminal event listener failed")) + return Err(Error::from(e).context("Terminal event listener failed")); } } } diff --git a/src/watch/debounce_event.rs b/src/watch/notify_event.rs similarity index 84% rename from src/watch/debounce_event.rs rename to src/watch/notify_event.rs index 1dc92cb4..0c8d6692 100644 --- a/src/watch/debounce_event.rs +++ b/src/watch/notify_event.rs @@ -1,13 +1,11 @@ use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::sync::mpsc::Sender; - -use crate::exercise::Exercise; +use std::{path::Path, sync::mpsc::Sender}; use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercises: &'static [Exercise], + pub exercise_paths: &'static [&'static Path], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { @@ -23,9 +21,9 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { return None; } - self.exercises + self.exercise_paths .iter() - .position(|exercise| event.path.ends_with(&exercise.path)) + .position(|path| event.path.ends_with(path)) }) .min() else { From bee62c89de09fdd9823cba81e07f0f8528fe8ef9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 02:41:19 +0200 Subject: [PATCH 133/433] Add terminal links --- src/app_state.rs | 2 +- src/app_state/state_file.rs | 8 +++----- src/embedded.rs | 7 ++++++- src/exercise.rs | 34 +++++++++++++++++++++++++++++++--- src/info_file.rs | 10 ++++------ src/list/state.rs | 2 +- src/run.rs | 12 +++++++++--- src/watch.rs | 2 +- src/watch/notify_event.rs | 4 ++-- src/watch/state.rs | 6 +----- 10 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 1a051b97..98c63842 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -38,7 +38,7 @@ impl AppState { // Leaking to be able to borrow in the watch mode `Table`. // Leaking is not a problem because the `AppState` instance lives until // the end of the program. - let path = Box::leak(exercise_info.path().into_boxed_path()); + let path = exercise_info.path().leak(); exercise_info.name.shrink_to_fit(); let name = exercise_info.name.leak(); diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs index 364a1fa3..4e4a0e15 100644 --- a/src/app_state/state_file.rs +++ b/src/app_state/state_file.rs @@ -59,7 +59,7 @@ pub fn write(app_state: &AppState) -> Result<()> { exercises: ExercisesStateSerializer(&app_state.exercises), }; - let mut buf = Vec::with_capacity(1024); + let mut buf = Vec::with_capacity(4096); serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?; fs::write(STATE_FILE_NAME, buf) .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?; @@ -69,8 +69,6 @@ pub fn write(app_state: &AppState) -> Result<()> { #[cfg(test)] mod tests { - use std::path::Path; - use crate::info_file::Mode; use super::*; @@ -81,14 +79,14 @@ mod tests { let exercises = [ Exercise { name: "1", - path: Path::new("exercises/1.rs"), + path: "exercises/1.rs", mode: Mode::Run, hint: String::new(), done: true, }, Exercise { name: "2", - path: Path::new("exercises/2.rs"), + path: "exercises/2.rs", mode: Mode::Test, hint: String::new(), done: false, diff --git a/src/embedded.rs b/src/embedded.rs index 1e2d6770..866b12b8 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -91,7 +91,12 @@ impl EmbeddedFiles { Ok(()) } - pub fn write_exercise_to_disk(&self, path: &Path, strategy: WriteStrategy) -> io::Result<()> { + pub fn write_exercise_to_disk

(&self, path: P, strategy: WriteStrategy) -> io::Result<()> + where + P: AsRef, + { + let path = path.as_ref(); + if let Some(file) = self .exercises_dir .files diff --git a/src/exercise.rs b/src/exercise.rs index c5ece5f5..2ec8d979 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; +use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - path::Path, + fs, process::{Command, Output}, }; @@ -10,11 +11,32 @@ use crate::{ info_file::Mode, }; +pub struct TerminalFileLink<'a> { + path: &'a str, +} + +impl<'a> Display for TerminalFileLink<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Ok(Some(canonical_path)) = fs::canonicalize(self.path) + .as_deref() + .map(|path| path.to_str()) + { + write!( + f, + "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", + canonical_path, self.path, + ) + } else { + write!(f, "{}", self.path,) + } + } +} + pub struct Exercise { // Exercise's unique name pub name: &'static str, // Exercise's path - pub path: &'static Path, + pub path: &'static str, // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise @@ -60,10 +82,16 @@ impl Exercise { .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) .with_context(|| format!("Failed to reset the exercise {self}")) } + + pub fn terminal_link(&self) -> StyledContent> { + style(TerminalFileLink { path: self.path }) + .underlined() + .blue() + } } impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(&self.path.display(), f) + self.path.fmt(f) } } diff --git a/src/info_file.rs b/src/info_file.rs index dc97b926..2a45e02d 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; -use std::{fs, path::PathBuf}; +use std::fs; // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] @@ -28,14 +28,12 @@ pub struct ExerciseInfo { } impl ExerciseInfo { - pub fn path(&self) -> PathBuf { - let path = if let Some(dir) = &self.dir { + pub fn path(&self) -> String { + if let Some(dir) = &self.dir { format!("exercises/{dir}/{}.rs", self.name) } else { format!("exercises/{}.rs", self.name) - }; - - PathBuf::from(path) + } } } diff --git a/src/list/state.rs b/src/list/state.rs index 38391a49..2a1fef18 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -63,7 +63,7 @@ impl<'a> UiState<'a> { next, exercise_state, Span::raw(exercise.name), - Span::raw(exercise.path.to_string_lossy()), + Span::raw(exercise.path), ])) }); diff --git a/src/run.rs b/src/run.rs index 9c504b53..863b584e 100644 --- a/src/run.rs +++ b/src/run.rs @@ -17,18 +17,24 @@ pub fn run(app_state: &mut AppState) -> Result<()> { if !output.status.success() { app_state.set_pending(app_state.current_exercise_ind())?; - bail!("Ran {} with errors", app_state.current_exercise()); + bail!( + "Ran {} with errors", + app_state.current_exercise().terminal_link(), + ); } stdout.write_fmt(format_args!( "{}{}\n", "✓ Successfully ran ".green(), - exercise.path.to_string_lossy().green(), + exercise.path.green(), ))?; match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::Pending => println!("Next exercise: {}", app_state.current_exercise()), + ExercisesProgress::Pending => println!( + "Next exercise: {}", + app_state.current_exercise().terminal_link(), + ), } Ok(()) diff --git a/src/watch.rs b/src/watch.rs index 58e829f3..bab64ae1 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,7 +42,7 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - exercise_paths: &'static [&'static Path], + exercise_paths: &'static [&'static str], ) -> Result { let (tx, rx) = channel(); let mut debouncer = new_debouncer( diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 0c8d6692..fb9a8c05 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,11 +1,11 @@ use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::{path::Path, sync::mpsc::Sender}; +use std::sync::mpsc::Sender; use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercise_paths: &'static [&'static Path], + pub exercise_paths: &'static [&'static str], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { diff --git a/src/watch/state.rs b/src/watch/state.rs index 6a97637b..1a79573f 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -136,11 +136,7 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise )?; self.writer.write_fmt(format_args!( "{progress_bar}Current exercise: {}\n", - self.app_state - .current_exercise() - .path - .to_string_lossy() - .bold(), + self.app_state.current_exercise().terminal_link(), ))?; self.show_prompt()?; From 9831cbb13975cd0f5ee4c295156102e3573ede1a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 03:13:33 +0200 Subject: [PATCH 134/433] Fix tests --- gen-dev-cargo-toml/src/main.rs | 24 ++++++++++++--------- tests/dev_cargo_bins.rs | 37 +++++++++++++++++++-------------- tests/fixture/failure/info.toml | 4 +--- tests/fixture/state/info.toml | 7 ++----- tests/fixture/success/info.toml | 4 +--- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs index 9a7c1bbd..792fe5f0 100644 --- a/gen-dev-cargo-toml/src/main.rs +++ b/gen-dev-cargo-toml/src/main.rs @@ -10,18 +10,18 @@ use std::{ }; #[derive(Deserialize)] -struct Exercise { +struct ExerciseInfo { name: String, - path: String, + dir: Option, } #[derive(Deserialize)] -struct InfoToml { - exercises: Vec, +struct InfoFile { + exercises: Vec, } fn main() -> Result<()> { - let exercises = toml_edit::de::from_str::( + let exercise_infos = toml_edit::de::from_str::( &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?, ) .context("Failed to deserialize `info.toml`")? @@ -36,12 +36,16 @@ fn main() -> Result<()> { bin = [\n", ); - for exercise in exercises { + for exercise_info in exercise_infos { buf.extend_from_slice(b" { name = \""); - buf.extend_from_slice(exercise.name.as_bytes()); - buf.extend_from_slice(b"\", path = \"../"); - buf.extend_from_slice(exercise.path.as_bytes()); - buf.extend_from_slice(b"\" },\n"); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"\", path = \"../exercises/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.extend_from_slice(b"/"); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); } buf.extend_from_slice( diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs index c3faea92..81f48b1e 100644 --- a/tests/dev_cargo_bins.rs +++ b/tests/dev_cargo_bins.rs @@ -5,34 +5,39 @@ use serde::Deserialize; use std::fs; #[derive(Deserialize)] -struct Exercise { +struct ExerciseInfo { name: String, - path: String, + dir: Option, } #[derive(Deserialize)] -struct InfoToml { - exercises: Vec, +struct InfoFile { + exercises: Vec, } #[test] fn dev_cargo_bins() { - let content = fs::read_to_string("dev/Cargo.toml").unwrap(); + let cargo_toml = fs::read_to_string("dev/Cargo.toml").unwrap(); - let exercises = toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) - .unwrap() - .exercises; + let exercise_infos = + toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) + .unwrap() + .exercises; let mut start_ind = 0; - for exercise in exercises { - let name_start = start_ind + content[start_ind..].find('"').unwrap() + 1; - let name_end = name_start + content[name_start..].find('"').unwrap(); - assert_eq!(exercise.name, &content[name_start..name_end]); + for exercise_info in exercise_infos { + let name_start = start_ind + cargo_toml[start_ind..].find('"').unwrap() + 1; + let name_end = name_start + cargo_toml[name_start..].find('"').unwrap(); + assert_eq!(exercise_info.name, &cargo_toml[name_start..name_end]); - // +3 to skip `../` at the begeinning of the path. - let path_start = name_end + content[name_end + 1..].find('"').unwrap() + 5; - let path_end = path_start + content[path_start..].find('"').unwrap(); - assert_eq!(exercise.path, &content[path_start..path_end]); + let path_start = name_end + cargo_toml[name_end + 1..].find('"').unwrap() + 2; + let path_end = path_start + cargo_toml[path_start..].find('"').unwrap(); + let expected_path = if let Some(dir) = exercise_info.dir { + format!("../exercises/{dir}/{}.rs", exercise_info.name) + } else { + format!("../exercises/{}.rs", exercise_info.name) + }; + assert_eq!(expected_path, &cargo_toml[path_start..path_end]); start_ind = path_end + 1; } diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml index 9474ee3f..94ec6ead 100644 --- a/tests/fixture/failure/info.toml +++ b/tests/fixture/failure/info.toml @@ -1,11 +1,9 @@ [[exercises]] name = "compFailure" -path = "exercises/compFailure.rs" -mode = "compile" +mode = "run" hint = "" [[exercises]] name = "testFailure" -path = "exercises/testFailure.rs" mode = "test" hint = "Hello!" diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml index 8de5d604..e5c4d8f4 100644 --- a/tests/fixture/state/info.toml +++ b/tests/fixture/state/info.toml @@ -1,17 +1,14 @@ [[exercises]] name = "pending_exercise" -path = "exercises/pending_exercise.rs" -mode = "compile" +mode = "run" hint = """""" [[exercises]] name = "pending_test_exercise" -path = "exercises/pending_test_exercise.rs" mode = "test" hint = """""" [[exercises]] name = "finished_exercise" -path = "exercises/finished_exercise.rs" -mode = "compile" +mode = "run" hint = """""" diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml index 17ed8c62..674ba264 100644 --- a/tests/fixture/success/info.toml +++ b/tests/fixture/success/info.toml @@ -1,11 +1,9 @@ [[exercises]] name = "compSuccess" -path = "exercises/compSuccess.rs" -mode = "compile" +mode = "run" hint = """""" [[exercises]] name = "testSuccess" -path = "exercises/testSuccess.rs" mode = "test" hint = """""" From 9dcc4b7df5f539b10117e97870a9f1cb01ca040d Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 05:13:27 +0200 Subject: [PATCH 135/433] Simplify the state file --- .gitignore | 2 +- Cargo.lock | 12 ---- Cargo.toml | 1 - src/app_state.rs | 128 +++++++++++++++++++++++------------- src/app_state/state_file.rs | 110 ------------------------------- src/init.rs | 2 +- 6 files changed, 86 insertions(+), 169 deletions(-) delete mode 100644 src/app_state/state_file.rs diff --git a/.gitignore b/.gitignore index c9172e01..80f9092a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ target/ /dev/Cargo.lock # State file -.rustlings-state.json +.rustlings-state.txt # oranda public/ diff --git a/Cargo.lock b/Cargo.lock index dbf1923e..6bc68f0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,7 +690,6 @@ dependencies = [ "ratatui", "rustlings-macros", "serde", - "serde_json", "toml_edit", "which", ] @@ -749,17 +748,6 @@ dependencies = [ "syn 2.0.58", ] -[[package]] -name = "serde_json" -version = "1.0.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" -dependencies = [ - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_spanned" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index 14ae9a14..07865abc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,6 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" rustlings-macros = { path = "rustlings-macros" } -serde_json = "1.0.115" serde.workspace = true toml_edit.workspace = true which = "6.0.1" diff --git a/src/app_state.rs b/src/app_state.rs index 98c63842..9a378de6 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -4,15 +4,14 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; -use std::io::{StdoutLock, Write}; - -mod state_file; +use std::{ + fs::{self, File}, + io::{Read, StdoutLock, Write}, +}; use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE}; -use self::state_file::{write, StateFileDeser}; - -const STATE_FILE_NAME: &str = ".rustlings-state.json"; +const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; #[must_use] @@ -27,11 +26,51 @@ pub struct AppState { n_done: u16, welcome_message: String, final_message: String, + file_buf: Vec, } impl AppState { + fn update_from_file(&mut self) { + self.file_buf.clear(); + self.n_done = 0; + + if File::open(STATE_FILE_NAME) + .and_then(|mut file| file.read_to_end(&mut self.file_buf)) + .is_ok() + { + let mut lines = self.file_buf.split(|c| *c == b'\n'); + let Some(current_exercise_name) = lines.next() else { + return; + }; + + if lines.next().is_none() { + return; + } + + let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); + + for done_exerise_name in lines { + if done_exerise_name.is_empty() { + break; + } + done_exercises.insert(done_exerise_name); + } + + for (ind, exercise) in self.exercises.iter_mut().enumerate() { + if done_exercises.contains(exercise.name.as_bytes()) { + exercise.done = true; + self.n_done += 1; + } + + if exercise.name.as_bytes() == current_exercise_name { + self.current_exercise_ind = ind; + } + } + } + } + pub fn new(info_file: InfoFile) -> Self { - let mut exercises = info_file + let exercises = info_file .exercises .into_iter() .map(|mut exercise_info| { @@ -55,42 +94,18 @@ impl AppState { }) .collect::>(); - let (current_exercise_ind, n_done) = StateFileDeser::read().map_or((0, 0), |state_file| { - let mut state_file_exercises = - hashbrown::HashMap::with_capacity(state_file.exercises.len()); - - for (ind, exercise_state) in state_file.exercises.into_iter().enumerate() { - state_file_exercises.insert( - exercise_state.name, - (ind == state_file.current_exercise_ind, exercise_state.done), - ); - } - - let mut current_exercise_ind = 0; - let mut n_done = 0; - for (ind, exercise) in exercises.iter_mut().enumerate() { - if let Some((current, done)) = state_file_exercises.get(exercise.name) { - if *done { - exercise.done = true; - n_done += 1; - } - - if *current { - current_exercise_ind = ind; - } - } - } - - (current_exercise_ind, n_done) - }); - - Self { - current_exercise_ind, + let mut slf = Self { + current_exercise_ind: 0, exercises, - n_done, + n_done: 0, welcome_message: info_file.welcome_message.unwrap_or_default(), final_message: info_file.final_message.unwrap_or_default(), - } + file_buf: Vec::with_capacity(2048), + }; + + slf.update_from_file(); + + slf } #[inline] @@ -120,7 +135,7 @@ impl AppState { self.current_exercise_ind = ind; - write(self) + self.write() } pub fn set_current_exercise_by_name(&mut self, name: &str) -> Result<()> { @@ -132,7 +147,7 @@ impl AppState { .position(|exercise| exercise.name == name) .with_context(|| format!("No exercise found for '{name}'!"))?; - write(self) + self.write() } pub fn set_pending(&mut self, ind: usize) -> Result<()> { @@ -141,7 +156,7 @@ impl AppState { if exercise.done { exercise.done = false; self.n_done -= 1; - write(self)?; + self.write()?; } Ok(()) @@ -193,7 +208,7 @@ impl AppState { self.exercises[exercise_ind].done = false; self.n_done -= 1; - write(self)?; + self.write()?; return Ok(ExercisesProgress::Pending); } @@ -213,6 +228,31 @@ impl AppState { Ok(ExercisesProgress::Pending) } + + // Write the state file. + // The file's format is very simple: + // - The first line is the name of the current exercise. + // - The second line is an empty line. + // - All remaining lines are the names of done exercises. + fn write(&mut self) -> Result<()> { + self.file_buf.clear(); + + self.file_buf + .extend_from_slice(self.current_exercise().name.as_bytes()); + self.file_buf.extend_from_slice(b"\n\n"); + + for exercise in &self.exercises { + if exercise.done { + self.file_buf.extend_from_slice(exercise.name.as_bytes()); + self.file_buf.extend_from_slice(b"\n"); + } + } + + fs::write(STATE_FILE_NAME, &self.file_buf) + .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; + + Ok(()) + } } const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" diff --git a/src/app_state/state_file.rs b/src/app_state/state_file.rs deleted file mode 100644 index 4e4a0e15..00000000 --- a/src/app_state/state_file.rs +++ /dev/null @@ -1,110 +0,0 @@ -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::fs; - -use crate::exercise::Exercise; - -use super::{AppState, STATE_FILE_NAME}; - -#[derive(Deserialize)] -pub struct ExerciseStateDeser { - pub name: String, - pub done: bool, -} - -#[derive(Serialize)] -struct ExerciseStateSer<'a> { - name: &'a str, - done: bool, -} - -struct ExercisesStateSerializer<'a>(&'a [Exercise]); - -impl<'a> Serialize for ExercisesStateSerializer<'a> { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let iter = self.0.iter().map(|exercise| ExerciseStateSer { - name: exercise.name, - done: exercise.done, - }); - - serializer.collect_seq(iter) - } -} - -#[derive(Deserialize)] -pub struct StateFileDeser { - pub current_exercise_ind: usize, - pub exercises: Vec, -} - -#[derive(Serialize)] -struct StateFileSer<'a> { - current_exercise_ind: usize, - exercises: ExercisesStateSerializer<'a>, -} - -impl StateFileDeser { - pub fn read() -> Option { - let file_content = fs::read(STATE_FILE_NAME).ok()?; - serde_json::de::from_slice(&file_content).ok() - } -} - -pub fn write(app_state: &AppState) -> Result<()> { - let content = StateFileSer { - current_exercise_ind: app_state.current_exercise_ind, - exercises: ExercisesStateSerializer(&app_state.exercises), - }; - - let mut buf = Vec::with_capacity(4096); - serde_json::ser::to_writer(&mut buf, &content).context("Failed to serialize the state")?; - fs::write(STATE_FILE_NAME, buf) - .with_context(|| format!("Failed to write the state file `{STATE_FILE_NAME}`"))?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::info_file::Mode; - - use super::*; - - #[test] - fn ser_deser_sync() { - let current_exercise_ind = 1; - let exercises = [ - Exercise { - name: "1", - path: "exercises/1.rs", - mode: Mode::Run, - hint: String::new(), - done: true, - }, - Exercise { - name: "2", - path: "exercises/2.rs", - mode: Mode::Test, - hint: String::new(), - done: false, - }, - ]; - - let ser = StateFileSer { - current_exercise_ind, - exercises: ExercisesStateSerializer(&exercises), - }; - let deser: StateFileDeser = - serde_json::de::from_slice(&serde_json::ser::to_vec(&ser).unwrap()).unwrap(); - - assert_eq!(deser.current_exercise_ind, current_exercise_ind); - assert!(deser - .exercises - .iter() - .zip(exercises) - .all(|(deser, ser)| deser.name == ser.name && deser.done == ser.done)); - } -} diff --git a/src/init.rs b/src/init.rs index 2badf376..4ee503a2 100644 --- a/src/init.rs +++ b/src/init.rs @@ -89,7 +89,7 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { } const GITIGNORE: &[u8] = b"/target -/.rustlings-state.json +/.rustlings-state.txt "; const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; From 1c90575b9fe0f0fb32006e000aefff10d8a4a39c Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 05:13:50 +0200 Subject: [PATCH 136/433] Update deps --- Cargo.lock | 59 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bc68f0d..5cfebe60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,9 +261,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "equivalent" @@ -1000,7 +1000,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1020,17 +1020,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -1041,9 +1042,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -1053,9 +1054,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -1065,9 +1066,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -1077,9 +1084,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -1089,9 +1096,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -1101,9 +1108,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -1113,9 +1120,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" From 3da860927d131eacc288764672ed8799a6a8cfca Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 14:53:32 +0200 Subject: [PATCH 137/433] Use push instead of extend_from_slice on chars --- gen-dev-cargo-toml/src/main.rs | 2 +- src/app_state.rs | 2 +- src/init.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs index 792fe5f0..43b4ebd7 100644 --- a/gen-dev-cargo-toml/src/main.rs +++ b/gen-dev-cargo-toml/src/main.rs @@ -42,7 +42,7 @@ bin = [\n", buf.extend_from_slice(b"\", path = \"../exercises/"); if let Some(dir) = &exercise_info.dir { buf.extend_from_slice(dir.as_bytes()); - buf.extend_from_slice(b"/"); + buf.push(b'/'); } buf.extend_from_slice(exercise_info.name.as_bytes()); buf.extend_from_slice(b".rs\" },\n"); diff --git a/src/app_state.rs b/src/app_state.rs index 9a378de6..31cb2cbf 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -244,7 +244,7 @@ impl AppState { for exercise in &self.exercises { if exercise.done { self.file_buf.extend_from_slice(exercise.name.as_bytes()); - self.file_buf.extend_from_slice(b"\n"); + self.file_buf.push(b'\n'); } } diff --git a/src/init.rs b/src/init.rs index 4ee503a2..459519de 100644 --- a/src/init.rs +++ b/src/init.rs @@ -17,7 +17,7 @@ fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { cargo_toml.extend_from_slice(b"\", path = \"exercises/"); if let Some(dir) = &exercise_info.dir { cargo_toml.extend_from_slice(dir.as_bytes()); - cargo_toml.extend_from_slice(b"/"); + cargo_toml.push(b'/'); } cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); cargo_toml.extend_from_slice(b".rs\" },\n"); From 8aef915ee732af1480cd7b93818f7d71c3ba178c Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 16:03:49 +0200 Subject: [PATCH 138/433] Show the welcome message --- src/app_state.rs | 87 +++++++++++++++++++++++++++--------------------- src/main.rs | 32 ++++++++++++++++-- 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 31cb2cbf..fb4b92e7 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -9,7 +9,7 @@ use std::{ io::{Read, StdoutLock, Write}, }; -use crate::{exercise::Exercise, info_file::InfoFile, FENISH_LINE}; +use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -20,58 +20,69 @@ pub enum ExercisesProgress { Pending, } +pub enum StateFileStatus { + Read, + NotRead, +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, n_done: u16, - welcome_message: String, final_message: String, file_buf: Vec, } impl AppState { - fn update_from_file(&mut self) { + fn update_from_file(&mut self) -> StateFileStatus { self.file_buf.clear(); self.n_done = 0; if File::open(STATE_FILE_NAME) .and_then(|mut file| file.read_to_end(&mut self.file_buf)) - .is_ok() + .is_err() { - let mut lines = self.file_buf.split(|c| *c == b'\n'); - let Some(current_exercise_name) = lines.next() else { - return; - }; + return StateFileStatus::NotRead; + } - if lines.next().is_none() { - return; + // See `Self::write` for more information about the file format. + let mut lines = self.file_buf.split(|c| *c == b'\n'); + let Some(current_exercise_name) = lines.next() else { + return StateFileStatus::NotRead; + }; + + if current_exercise_name.is_empty() || lines.next().is_none() { + return StateFileStatus::NotRead; + } + + let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); + + for done_exerise_name in lines { + if done_exerise_name.is_empty() { + break; + } + done_exercises.insert(done_exerise_name); + } + + for (ind, exercise) in self.exercises.iter_mut().enumerate() { + if done_exercises.contains(exercise.name.as_bytes()) { + exercise.done = true; + self.n_done += 1; } - let mut done_exercises = hashbrown::HashSet::with_capacity(self.exercises.len()); - - for done_exerise_name in lines { - if done_exerise_name.is_empty() { - break; - } - done_exercises.insert(done_exerise_name); - } - - for (ind, exercise) in self.exercises.iter_mut().enumerate() { - if done_exercises.contains(exercise.name.as_bytes()) { - exercise.done = true; - self.n_done += 1; - } - - if exercise.name.as_bytes() == current_exercise_name { - self.current_exercise_ind = ind; - } + if exercise.name.as_bytes() == current_exercise_name { + self.current_exercise_ind = ind; } } + + StateFileStatus::Read } - pub fn new(info_file: InfoFile) -> Self { - let exercises = info_file - .exercises + pub fn new( + exercise_infos: Vec, + final_message: String, + ) -> (Self, StateFileStatus) { + let exercises = exercise_infos .into_iter() .map(|mut exercise_info| { // Leaking to be able to borrow in the watch mode `Table`. @@ -98,14 +109,13 @@ impl AppState { current_exercise_ind: 0, exercises, n_done: 0, - welcome_message: info_file.welcome_message.unwrap_or_default(), - final_message: info_file.final_message.unwrap_or_default(), + final_message, file_buf: Vec::with_capacity(2048), }; - slf.update_from_file(); + let state_file_status = slf.update_from_file(); - slf + (slf, state_file_status) } #[inline] @@ -231,7 +241,8 @@ impl AppState { // Write the state file. // The file's format is very simple: - // - The first line is the name of the current exercise. + // - The first line is the name of the current exercise. It must end with `\n` even if there + // are no done exercises. // - The second line is an empty line. // - All remaining lines are the names of done exercises. fn write(&mut self) -> Result<()> { @@ -239,12 +250,12 @@ impl AppState { self.file_buf .extend_from_slice(self.current_exercise().name.as_bytes()); - self.file_buf.extend_from_slice(b"\n\n"); + self.file_buf.push(b'\n'); for exercise in &self.exercises { if exercise.done { - self.file_buf.extend_from_slice(exercise.name.as_bytes()); self.file_buf.push(b'\n'); + self.file_buf.extend_from_slice(exercise.name.as_bytes()); } } diff --git a/src/main.rs b/src/main.rs index a96e3230..aeb94321 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,15 @@ use anyhow::{Context, Result}; +use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use std::{path::Path, process::exit}; +use crossterm::{ + terminal::{Clear, ClearType}, + ExecutableCommand, +}; +use std::{ + io::{self, BufRead, Write}, + path::Path, + process::exit, +}; mod app_state; mod embedded; @@ -67,7 +76,26 @@ fn main() -> Result<()> { exit(1); } - let mut app_state = AppState::new(info_file); + let (mut app_state, state_file_status) = AppState::new( + info_file.exercises, + info_file.final_message.unwrap_or_default(), + ); + + if let Some(welcome_message) = info_file.welcome_message { + match state_file_status { + StateFileStatus::NotRead => { + let mut stdout = io::stdout().lock(); + stdout.execute(Clear(ClearType::All))?; + + let welcome_message = welcome_message.trim(); + write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; + stdout.flush()?; + + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + } + StateFileStatus::Read => (), + } + } match args.command { None => { From 070a780d7f7ca4ef03ab29898ec553933994bfab Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 16:04:05 +0200 Subject: [PATCH 139/433] Trim the final message --- src/app_state.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index fb4b92e7..432a9a27 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -228,8 +228,12 @@ impl AppState { writer.execute(Clear(ClearType::All))?; writer.write_all(FENISH_LINE.as_bytes())?; - writer.write_all(self.final_message.as_bytes())?; - writer.write_all(b"\n")?; + + let final_message = self.final_message.trim(); + if !final_message.is_empty() { + writer.write_all(self.final_message.as_bytes())?; + writer.write_all(b"\n")?; + } return Ok(ExercisesProgress::AllDone); }; From bd10b154fe558af693e9f8f57dbb3e43f0bd0ec8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 16:07:17 +0200 Subject: [PATCH 140/433] Clear the terminal after showing the welcome message --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index aeb94321..67969215 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,6 +92,8 @@ fn main() -> Result<()> { stdout.flush()?; io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + + stdout.execute(Clear(ClearType::All))?; } StateFileStatus::Read => (), } From 1cbabc3d28a29a01caeffba969ed640e00e5f0be Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 17:10:53 +0200 Subject: [PATCH 141/433] Add the manual-run option --- src/main.rs | 28 +++++++++++++------- src/watch.rs | 51 +++++++++++++++++++++++++++---------- src/watch/state.rs | 8 +++++- src/watch/terminal_event.rs | 4 ++- 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index 67969215..28a426b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,10 @@ use self::{ struct Args { #[command(subcommand)] command: Option, + /// Manually run the current exercise using `r` or `run` in the watch mode. + /// Only use this if Rustlings fails to detect exercise file changes. + #[arg(long)] + manual_run: bool, } #[derive(Subcommand)] @@ -101,17 +105,23 @@ fn main() -> Result<()> { match args.command { None => { - // For the the notify event handler thread. - // Leaking is not a problem because the slice lives until the end of the program. - let exercise_paths = app_state - .exercises() - .iter() - .map(|exercise| exercise.path) - .collect::>() - .leak(); + let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run { + None + } else { + // For the the notify event handler thread. + // Leaking is not a problem because the slice lives until the end of the program. + Some( + app_state + .exercises() + .iter() + .map(|exercise| exercise.path) + .collect::>() + .leak(), + ) + }; loop { - match watch(&mut app_state, exercise_paths)? { + match watch(&mut app_state, notify_exercise_paths)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the diff --git a/src/watch.rs b/src/watch.rs index bab64ae1..d20e552e 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,25 +42,38 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - exercise_paths: &'static [&'static str], + notify_exercise_paths: Option<&'static [&'static str]>, ) -> Result { let (tx, rx) = channel(); - let mut debouncer = new_debouncer( - Duration::from_secs(1), - DebounceEventHandler { - tx: tx.clone(), - exercise_paths, - }, - )?; - debouncer - .watcher() - .watch(Path::new("exercises"), RecursiveMode::Recursive)?; - let mut watch_state = WatchState::new(app_state); + let mut manual_run = false; + // Prevent dropping the guard until the end of the function. + // Otherwise, the file watcher exits. + let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { + let mut debouncer = new_debouncer( + Duration::from_secs(1), + DebounceEventHandler { + tx: tx.clone(), + exercise_paths, + }, + ) + .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; + debouncer + .watcher() + .watch(Path::new("exercises"), RecursiveMode::Recursive) + .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; + + Some(debouncer) + } else { + manual_run = true; + None + }; + + let mut watch_state = WatchState::new(app_state, manual_run); watch_state.run_current_exercise()?; - thread::spawn(move || terminal_event_handler(tx)); + thread::spawn(move || terminal_event_handler(tx, manual_run)); while let Ok(event) = rx.recv() { match event { @@ -78,6 +91,7 @@ pub fn watch( watch_state.into_writer().write_all(QUIT_MSG)?; break; } + WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { watch_state.handle_invalid_cmd(&cmd)?; } @@ -88,7 +102,8 @@ pub fn watch( watch_state.render()?; } WatchEvent::NotifyErr(e) => { - return Err(Error::from(e).context("Exercise file watcher failed")); + watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?; + return Err(Error::from(e)); } WatchEvent::TerminalEventErr(e) => { return Err(Error::from(e).context("Terminal event listener failed")); @@ -103,3 +118,11 @@ const QUIT_MSG: &[u8] = b" We hope you're enjoying learning Rust! If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. "; + +const NOTIFY_ERR: &str = " +The automatic detection of exercise file changes failed :( +Please try running `rustlings` again. + +If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. +You need to manually trigger running the current exercise using `r` or `run` then. +"; diff --git a/src/watch/state.rs b/src/watch/state.rs index 1a79573f..c0f6c532 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -18,10 +18,11 @@ pub struct WatchState<'a> { stderr: Option>, show_hint: bool, show_done: bool, + manual_run: bool, } impl<'a> WatchState<'a> { - pub fn new(app_state: &'a mut AppState) -> Self { + pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { let writer = io::stdout().lock(); Self { @@ -31,6 +32,7 @@ impl<'a> WatchState<'a> { stderr: None, show_hint: false, show_done: false, + manual_run, } } @@ -78,6 +80,10 @@ impl<'a> WatchState<'a> { fn show_prompt(&mut self) -> io::Result<()> { self.writer.write_all(b"\n")?; + if self.manual_run { + self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; + } + if self.show_done { self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 7f7ebe06..6d790b7c 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -4,6 +4,7 @@ use std::sync::mpsc::Sender; use super::WatchEvent; pub enum InputEvent { + Run, Next, Hint, List, @@ -11,7 +12,7 @@ pub enum InputEvent { Unrecognized(String), } -pub fn terminal_event_handler(tx: Sender) { +pub fn terminal_event_handler(tx: Sender, manual_run: bool) { let mut input = String::with_capacity(8); let last_input_event = loop { @@ -43,6 +44,7 @@ pub fn terminal_event_handler(tx: Sender) { "h" | "hint" => InputEvent::Hint, "l" | "list" => break InputEvent::List, "q" | "quit" => break InputEvent::Quit, + "r" | "run" if manual_run => InputEvent::Run, _ => InputEvent::Unrecognized(input.clone()), }; From 7526c6b1f92626df6ab8b4853535b73711bfada4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 17:11:27 +0200 Subject: [PATCH 142/433] Update POST_INIT_MSG --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 28a426b7..ed5becf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -174,7 +174,7 @@ const POST_INIT_MSG: &str = " Done initialization! Run `cd rustlings` to go into the generated directory. -Then run `rustlings` for further instructions on getting started."; +Then run `rustlings` to get started."; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | From c613b70363c60c6f4305d09c7394c96cdc6b69e4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 14 Apr 2024 17:28:01 +0200 Subject: [PATCH 143/433] Print the trimmed final message --- src/app_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app_state.rs b/src/app_state.rs index 432a9a27..54c02d69 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -231,7 +231,7 @@ impl AppState { let final_message = self.final_message.trim(); if !final_message.is_empty() { - writer.write_all(self.final_message.as_bytes())?; + writer.write_all(final_message.as_bytes())?; writer.write_all(b"\n")?; } From 15ca847c37c170590abe6caa53dba5606d956341 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 15 Apr 2024 02:11:27 +0200 Subject: [PATCH 144/433] Implement third-party exercises trust handling --- Cargo.lock | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/init.rs | 4 ++- src/main.rs | 53 ++++++++++++++++++++++++++-------- src/trust.rs | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 src/trust.rs diff --git a/Cargo.lock b/Cargo.lock index 5cfebe60..671a03ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,6 +253,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -320,6 +341,17 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "getrandom" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -428,6 +460,16 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -528,6 +570,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -634,6 +682,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.4" @@ -684,6 +743,7 @@ dependencies = [ "assert_cmd", "clap", "crossterm", + "dirs", "hashbrown", "notify-debouncer-mini", "predicates", @@ -865,6 +925,26 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "toml_datetime" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index 07865abc..6cf9ef94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ edition.workspace = true anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" +dirs = "5.0.1" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" diff --git a/src/init.rs b/src/init.rs index 459519de..d051fc42 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,7 +6,7 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; +use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo, trust::trust_current_dir}; fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); @@ -85,6 +85,8 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + trust_current_dir()?; + Ok(()) } diff --git a/src/main.rs b/src/main.rs index ed5becf5..7b63f70f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use anyhow::{Context, Result}; -use app_state::StateFileStatus; use clap::{Parser, Subcommand}; use crossterm::{ terminal::{Clear, ClearType}, @@ -19,14 +18,16 @@ mod init; mod list; mod progress_bar; mod run; +mod trust; mod watch; use self::{ - app_state::AppState, + app_state::{AppState, StateFileStatus}, info_file::InfoFile, init::init, list::list, run::run, + trust::{current_dir_is_trusted, trust_current_dir}, watch::{watch, WatchExit}, }; @@ -61,6 +62,11 @@ enum Subcommands { /// The name of the exercise name: String, }, + /// Trust the current directory with its exercises. + /// + /// You only need to run this if you want to work on third-party exercises or after you moved + /// the official exercises that were initialized with `rustlings init`. + Trust, } fn main() -> Result<()> { @@ -72,14 +78,26 @@ fn main() -> Result<()> { if matches!(args.command, Some(Subcommands::Init)) { init(&info_file.exercises).context("Initialization failed")?; - println!("{POST_INIT_MSG}"); return Ok(()); - } else if !Path::new("exercises").is_dir() { + } + + if !Path::new("exercises").is_dir() { println!("{PRE_INIT_MSG}"); exit(1); } + if matches!(args.command, Some(Subcommands::Trust)) { + trust_current_dir()?; + println!("{POST_TRUST_MSG}"); + return Ok(()); + } + + if !current_dir_is_trusted()? { + println!("{NOT_TRUSTED_MSG}"); + exit(1); + } + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), @@ -130,8 +148,6 @@ fn main() -> Result<()> { } } } - // `Init` is handled above. - Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; @@ -149,6 +165,8 @@ fn main() -> Result<()> { app_state.set_current_exercise_by_name(&name)?; println!("{}", app_state.current_exercise().hint); } + // `Init` and `Trust` are handled above. + Some(Subcommands::Init | Subcommands::Trust) => (), } Ok(()) @@ -158,8 +176,13 @@ const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. Did you already install Rust? Try running `cargo --version` to diagnose the problem."; +const POST_INIT_MSG: &str = "Done initialization! + +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` to get started."; + const PRE_INIT_MSG: &str = r" - welcome to... + Welcome to... _ _ _ _ __ _ _ ___| |_| (_)_ __ __ _ ___ | '__| | | / __| __| | | '_ \ / _` / __| @@ -170,11 +193,19 @@ const PRE_INIT_MSG: &str = r" The `exercises` directory wasn't found in the current directory. If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; -const POST_INIT_MSG: &str = " -Done initialization! +const POST_TRUST_MSG: &str = "You now trust the exercises in the current directory. +Run `rustlings` to start working on them."; -Run `cd rustlings` to go into the generated directory. -Then run `rustlings` to get started."; +const NOT_TRUSTED_MSG: &str = "It looks like you are trying to work on third-party exercises. +Rustlings supports third-party exercises. But because Rustlings runs the code inside an exercise, +we need to warn you about the possibility of malicious code. +We recommend that you read all the exercise files in the `exercises` directory and check the +dependencies in the `Cargo.toml` file. +If everything looks fine and you want to trust this directory, run `rustlings trust`. + +If you you are trying to work on the official exercises that were generated using `rustlings init`, +then you probably moved the directory containing them. In that case, you can run `rustlings trust` +without a problem."; const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | diff --git a/src/trust.rs b/src/trust.rs new file mode 100644 index 00000000..7e36f737 --- /dev/null +++ b/src/trust.rs @@ -0,0 +1,72 @@ +use anyhow::{Context, Error, Result}; +use std::{ + env, + fs::{self, OpenOptions}, + io::{ErrorKind, Write}, +}; + +const DATA_DIR_NAME: &str = "rustlings"; +const TRUSTED_DIRS_FILE_NAME: &str = "trusted-dirs.txt"; + +pub fn trust_current_dir() -> Result<()> { + let mut path = dirs::data_dir().context("Failed to determine the data directory")?; + path.push(DATA_DIR_NAME); + if !path.is_dir() { + fs::create_dir(&path) + .with_context(|| format!("Failed to create the directory {}", path.display()))?; + } + + path.push(TRUSTED_DIRS_FILE_NAME); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&path) + .with_context(|| { + format!( + "Failed to create/open the file {} in write mode", + path.display(), + ) + })?; + + let dir = env::current_dir().context("Failed to get the current directory path")?; + let dir = dir.to_string_lossy(); + let mut line = Vec::with_capacity(dir.as_bytes().len() + 1); + line.extend_from_slice(dir.as_bytes()); + line.push(b'\n'); + + file.write_all(&line) + .with_context(|| format!("Failed to append to the file {}", path.display())) +} + +pub fn current_dir_is_trusted() -> Result { + let mut path = dirs::data_dir().context("Failed to determine the data directory")?; + path.push(DATA_DIR_NAME); + path.push(TRUSTED_DIRS_FILE_NAME); + + let content = match fs::read(&path) { + Ok(v) => v, + Err(e) => match e.kind() { + ErrorKind::NotFound => return Ok(false), + _ => { + return Err( + Error::from(e).context(format!("Failed to read the file {}", path.display())) + ) + } + }, + }; + + let current_dir = env::current_dir().context("Failed to get the current directory path")?; + let current_dir = current_dir.to_string_lossy(); + + for line in content.split(|c| *c == b'\n') { + if line.is_empty() { + break; + } + + if line == current_dir.as_bytes() { + return Ok(true); + } + } + + Ok(false) +} From 6f04570dd080f3aedf2fdf4fac1e627abe3a5b27 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 15 Apr 2024 03:36:12 +0200 Subject: [PATCH 145/433] Revert "Implement third-party exercises trust handling" This reverts commit 15ca847c37c170590abe6caa53dba5606d956341. See https://rust-lang.zulipchat.com/#narrow/stream/334454-rustlings/topic/Proposal.3A.20Third-party.20exercises/near/433183449 --- Cargo.lock | 80 ---------------------------------------------------- Cargo.toml | 1 - src/init.rs | 4 +-- src/main.rs | 39 +++---------------------- src/trust.rs | 72 ---------------------------------------------- 5 files changed, 5 insertions(+), 191 deletions(-) delete mode 100644 src/trust.rs diff --git a/Cargo.lock b/Cargo.lock index 671a03ff..5cfebe60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,27 +253,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "dirs" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.48.0", -] - [[package]] name = "doc-comment" version = "0.3.3" @@ -341,17 +320,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "getrandom" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -460,16 +428,6 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" -[[package]] -name = "libredox" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" -dependencies = [ - "bitflags 2.5.0", - "libc", -] - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -570,12 +528,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - [[package]] name = "parking_lot" version = "0.12.1" @@ -682,17 +634,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_users" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - [[package]] name = "regex" version = "1.10.4" @@ -743,7 +684,6 @@ dependencies = [ "assert_cmd", "clap", "crossterm", - "dirs", "hashbrown", "notify-debouncer-mini", "predicates", @@ -925,26 +865,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" -[[package]] -name = "thiserror" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.58", -] - [[package]] name = "toml_datetime" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index 6cf9ef94..07865abc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ edition.workspace = true anyhow.workspace = true clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" -dirs = "5.0.1" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.1" diff --git a/src/init.rs b/src/init.rs index d051fc42..459519de 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,7 +6,7 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo, trust::trust_current_dir}; +use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { let mut cargo_toml = Vec::with_capacity(1 << 13); @@ -85,8 +85,6 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; - trust_current_dir()?; - Ok(()) } diff --git a/src/main.rs b/src/main.rs index 7b63f70f..541783dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use app_state::StateFileStatus; use clap::{Parser, Subcommand}; use crossterm::{ terminal::{Clear, ClearType}, @@ -18,16 +19,14 @@ mod init; mod list; mod progress_bar; mod run; -mod trust; mod watch; use self::{ - app_state::{AppState, StateFileStatus}, + app_state::AppState, info_file::InfoFile, init::init, list::list, run::run, - trust::{current_dir_is_trusted, trust_current_dir}, watch::{watch, WatchExit}, }; @@ -62,11 +61,6 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// Trust the current directory with its exercises. - /// - /// You only need to run this if you want to work on third-party exercises or after you moved - /// the official exercises that were initialized with `rustlings init`. - Trust, } fn main() -> Result<()> { @@ -87,17 +81,6 @@ fn main() -> Result<()> { exit(1); } - if matches!(args.command, Some(Subcommands::Trust)) { - trust_current_dir()?; - println!("{POST_TRUST_MSG}"); - return Ok(()); - } - - if !current_dir_is_trusted()? { - println!("{NOT_TRUSTED_MSG}"); - exit(1); - } - let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), @@ -148,6 +131,8 @@ fn main() -> Result<()> { } } } + // `Init` is handled above. + Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; @@ -165,8 +150,6 @@ fn main() -> Result<()> { app_state.set_current_exercise_by_name(&name)?; println!("{}", app_state.current_exercise().hint); } - // `Init` and `Trust` are handled above. - Some(Subcommands::Init | Subcommands::Trust) => (), } Ok(()) @@ -193,20 +176,6 @@ const PRE_INIT_MSG: &str = r" The `exercises` directory wasn't found in the current directory. If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; -const POST_TRUST_MSG: &str = "You now trust the exercises in the current directory. -Run `rustlings` to start working on them."; - -const NOT_TRUSTED_MSG: &str = "It looks like you are trying to work on third-party exercises. -Rustlings supports third-party exercises. But because Rustlings runs the code inside an exercise, -we need to warn you about the possibility of malicious code. -We recommend that you read all the exercise files in the `exercises` directory and check the -dependencies in the `Cargo.toml` file. -If everything looks fine and you want to trust this directory, run `rustlings trust`. - -If you you are trying to work on the official exercises that were generated using `rustlings init`, -then you probably moved the directory containing them. In that case, you can run `rustlings trust` -without a problem."; - const FENISH_LINE: &str = "+----------------------------------------------------+ | You made it to the Fe-nish line! | +-------------------------- ------------------------+ diff --git a/src/trust.rs b/src/trust.rs deleted file mode 100644 index 7e36f737..00000000 --- a/src/trust.rs +++ /dev/null @@ -1,72 +0,0 @@ -use anyhow::{Context, Error, Result}; -use std::{ - env, - fs::{self, OpenOptions}, - io::{ErrorKind, Write}, -}; - -const DATA_DIR_NAME: &str = "rustlings"; -const TRUSTED_DIRS_FILE_NAME: &str = "trusted-dirs.txt"; - -pub fn trust_current_dir() -> Result<()> { - let mut path = dirs::data_dir().context("Failed to determine the data directory")?; - path.push(DATA_DIR_NAME); - if !path.is_dir() { - fs::create_dir(&path) - .with_context(|| format!("Failed to create the directory {}", path.display()))?; - } - - path.push(TRUSTED_DIRS_FILE_NAME); - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(&path) - .with_context(|| { - format!( - "Failed to create/open the file {} in write mode", - path.display(), - ) - })?; - - let dir = env::current_dir().context("Failed to get the current directory path")?; - let dir = dir.to_string_lossy(); - let mut line = Vec::with_capacity(dir.as_bytes().len() + 1); - line.extend_from_slice(dir.as_bytes()); - line.push(b'\n'); - - file.write_all(&line) - .with_context(|| format!("Failed to append to the file {}", path.display())) -} - -pub fn current_dir_is_trusted() -> Result { - let mut path = dirs::data_dir().context("Failed to determine the data directory")?; - path.push(DATA_DIR_NAME); - path.push(TRUSTED_DIRS_FILE_NAME); - - let content = match fs::read(&path) { - Ok(v) => v, - Err(e) => match e.kind() { - ErrorKind::NotFound => return Ok(false), - _ => { - return Err( - Error::from(e).context(format!("Failed to read the file {}", path.display())) - ) - } - }, - }; - - let current_dir = env::current_dir().context("Failed to get the current directory path")?; - let current_dir = current_dir.to_string_lossy(); - - for line in content.split(|c| *c == b'\n') { - if line.is_empty() { - break; - } - - if line == current_dir.as_bytes() { - return Ok(true); - } - } - - Ok(false) -} From f5eaa578b9479d6a0b9acab5ebf155fcd50b9b27 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 15 Apr 2024 23:35:30 +0200 Subject: [PATCH 146/433] Update deps --- Cargo.lock | 37 +++++++++++++------------------------ Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cfebe60..defdacd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,7 +179,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -589,9 +589,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" dependencies = [ "unicode-ident", ] @@ -607,9 +607,9 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8" +checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" dependencies = [ "bitflags 2.5.0", "cassowary", @@ -745,7 +745,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -795,12 +795,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "stability" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" dependencies = [ "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -834,25 +834,14 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.58", + "syn", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" dependencies = [ "proc-macro2", "quote", @@ -1156,5 +1145,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 07865abc..efc14419 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" -ratatui = "0.26.1" +ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros" } serde.workspace = true toml_edit.workspace = true From 7ebc260924f5db0099568589f2be621c9ea43721 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 15 Apr 2024 23:54:57 +0200 Subject: [PATCH 147/433] Scetch the dev subcommand --- src/dev.rs | 20 ++++++++++++++++++++ src/dev/check.rs | 5 +++++ src/dev/init.rs | 5 +++++ src/main.rs | 21 +++++++++------------ 4 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 src/dev.rs create mode 100644 src/dev/check.rs create mode 100644 src/dev/init.rs diff --git a/src/dev.rs b/src/dev.rs new file mode 100644 index 00000000..40382a87 --- /dev/null +++ b/src/dev.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use clap::Subcommand; + +mod check; +mod init; + +#[derive(Subcommand)] +pub enum DevCommands { + Init, + Check, +} + +impl DevCommands { + pub fn run(self) -> Result<()> { + match self { + DevCommands::Init => init::init(), + DevCommands::Check => check::check(), + } + } +} diff --git a/src/dev/check.rs b/src/dev/check.rs new file mode 100644 index 00000000..46d3ffec --- /dev/null +++ b/src/dev/check.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub fn check() -> Result<()> { + todo!() +} diff --git a/src/dev/init.rs b/src/dev/init.rs new file mode 100644 index 00000000..01cfd9f0 --- /dev/null +++ b/src/dev/init.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub fn init() -> Result<()> { + todo!() +} diff --git a/src/main.rs b/src/main.rs index 541783dc..e72dbdcc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use std::{ }; mod app_state; +mod dev; mod embedded; mod exercise; mod info_file; @@ -21,14 +22,7 @@ mod progress_bar; mod run; mod watch; -use self::{ - app_state::AppState, - info_file::InfoFile, - init::init, - list::list, - run::run, - watch::{watch, WatchExit}, -}; +use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] @@ -61,6 +55,8 @@ enum Subcommands { /// The name of the exercise name: String, }, + #[command(subcommand)] + Dev(DevCommands), } fn main() -> Result<()> { @@ -71,7 +67,7 @@ fn main() -> Result<()> { let info_file = InfoFile::parse()?; if matches!(args.command, Some(Subcommands::Init)) { - init(&info_file.exercises).context("Initialization failed")?; + init::init(&info_file.exercises).context("Initialization failed")?; println!("{POST_INIT_MSG}"); return Ok(()); } @@ -122,12 +118,12 @@ fn main() -> Result<()> { }; loop { - match watch(&mut app_state, notify_exercise_paths)? { + match watch::watch(&mut app_state, notify_exercise_paths)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the // watch state. - WatchExit::List => list(&mut app_state)?, + WatchExit::List => list::list(&mut app_state)?, } } } @@ -137,7 +133,7 @@ fn main() -> Result<()> { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; } - run(&mut app_state)?; + run::run(&mut app_state)?; } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; @@ -150,6 +146,7 @@ fn main() -> Result<()> { app_state.set_current_exercise_by_name(&name)?; println!("{}", app_state.current_exercise().hint); } + Some(Subcommands::Dev(dev_command)) => dev_command.run()?, } Ok(()) From 92777c0a4498625a44c0e6eeced97633dacc78d1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 01:22:54 +0200 Subject: [PATCH 148/433] Add the format version --- info.toml | 2 ++ src/info_file.rs | 1 + src/main.rs | 13 ++++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/info.toml b/info.toml index fa90ad70..27071a5c 100644 --- a/info.toml +++ b/info.toml @@ -1,3 +1,5 @@ +format_version = 1 + welcome_message = """Is this your first time? Don't worry, Rustlings was made for beginners! We are going to teach you a lot of things about Rust, but before we can get started, here's a couple of notes about how Rustlings operates: diff --git a/src/info_file.rs b/src/info_file.rs index 2a45e02d..18e77b9f 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -39,6 +39,7 @@ impl ExerciseInfo { #[derive(Deserialize)] pub struct InfoFile { + pub format_version: u8, pub welcome_message: Option, pub final_message: Option, pub exercises: Vec, diff --git a/src/main.rs b/src/main.rs index e72dbdcc..7d4d8a5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use app_state::StateFileStatus; use clap::{Parser, Subcommand}; use crossterm::{ @@ -24,6 +24,8 @@ mod watch; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; +const CURRENT_FORMAT_VERSION: u8 = 1; + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -66,6 +68,10 @@ fn main() -> Result<()> { let info_file = InfoFile::parse()?; + if info_file.format_version > CURRENT_FORMAT_VERSION { + bail!(FORMAT_VERSION_HIGHER_ERR); + } + if matches!(args.command, Some(Subcommands::Init)) { init::init(&info_file.exercises).context("Initialization failed")?; println!("{POST_INIT_MSG}"); @@ -156,6 +162,11 @@ const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. Did you already install Rust? Try running `cargo --version` to diagnose the problem."; +const FORMAT_VERSION_HIGHER_ERR: &str = + "The format version specified in the `info.toml` file is higher than the last one supported. +It is possible that you have an outdated version of Rustlings. +Try to install the latest Rustlings version first."; + const POST_INIT_MSG: &str = "Done initialization! Run `cd rustlings` to go into the generated directory. From 25e7696565349014c5e2662ddba43dc20391e272 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:08:45 +0200 Subject: [PATCH 149/433] Done `dev init` --- src/dev.rs | 7 +++- src/dev/init.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++- src/init.rs | 63 +++++++++++------------------- 3 files changed, 125 insertions(+), 45 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index 40382a87..e09996f8 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use clap::Subcommand; mod check; @@ -13,8 +13,11 @@ pub enum DevCommands { impl DevCommands { pub fn run(self) -> Result<()> { match self { - DevCommands::Init => init::init(), + DevCommands::Init => init::init().context(INIT_ERR), DevCommands::Check => check::check(), } } } + +const INIT_ERR: &str = "Initialization failed. +After resolving the issue, delete the `rustlings` directory (if it was created) and try again"; diff --git a/src/dev/init.rs b/src/dev/init.rs index 01cfd9f0..d3821362 100644 --- a/src/dev/init.rs +++ b/src/dev/init.rs @@ -1,5 +1,101 @@ -use anyhow::Result; +use std::fs::{self, create_dir}; + +use anyhow::{Context, Result}; + +use crate::CURRENT_FORMAT_VERSION; pub fn init() -> Result<()> { - todo!() + create_dir("rustlings").context("Failed to create the directory `rustlings`")?; + + create_dir("rustlings/exercises") + .context("Failed to create the directory `rustlings/exercises`")?; + + create_dir("rustlings/solutions") + .context("Failed to create the directory `rustlings/solutions`")?; + + fs::write( + "rustlings/info.toml", + format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"), + ) + .context("Failed to create the file `rustlings/info.toml`")?; + + fs::write( + "rustligns/Cargo.toml", + format!("{CARGO_TOML_COMMENT}{}", crate::init::CARGO_TOML_PACKAGE), + ) + .context("Failed to create the file `rustlings/Cargo.toml`")?; + + fs::write("rustlings/.gitignore", crate::init::GITIGNORE) + .context("Failed to create the file `rustlings/.gitignore`")?; + + fs::write("rustlings/README.md", README) + .context("Failed to create the file `rustlings/README.md`")?; + + create_dir("rustlings/.vscode") + .context("Failed to create the directory `rustligns/.vscode`")?; + fs::write( + "rustlings/.vscode/extensions.json", + crate::init::VS_CODE_EXTENSIONS_JSON, + ) + .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + + println!("{INIT_DONE}"); + + Ok(()) } + +const INFO_FILE_BEFORE_FORMAT_VERSION: &str = + "# The format version is an indicator of the compatibility of third-party exercises with the +# Rustlings program. +# The format version is not the same as the version of the Rustlings program. +# In case Rustlings makes an unavoidable breaking change to the expected format of third-party +# exercises, you would need to raise this version and adapt to the new format. +# Otherwise, the newest version of the Rustlings program won't be able to run these exercises. +format_version = "; + +const INFO_FILE_AFTER_FORMAT_VERSION: &str = r#" + +# Optional multi-line message to be shown to users when just starting with the exercises. +welcome_message = """Welcome to these third-party Rustlings exercises.""" + +# Optional multi-line message to be shown to users after finishing all exercises. +final_message = """We hope that you found the exercises helpful :D""" + +# Repeat this section for every exercise. +[[exercises]] +# Exercise name which is the exercise file name without the `.rs` extension. +name = "???" + +# Optional directory name to be provided if you want to organize exercises in directories. +# If `dir` is specified, the exercise path is `exercises/DIR/NAME.rs` +# Otherwise, the path is `exercises/NAME.rs` +# dir = "???" + +# A mutli-line hint to be shown to users on request. +hint = """???""" +"#; + +const CARGO_TOML_COMMENT: &str = + "# You shouldn't edit this file manually! It is updated by `rustlings dev check` + +"; + +const README: &str = "# Rustlings 🦀 + +Welcome to these third-party Rustlings exercises 😃 + +First, +[install Rustlings using the official instructions in the README of the Rustlings project](https://github.com/rust-lang/rustlings) ✅ + +Then, open your terminal in this directory and run `rustlings` to get started with the exercises 🚀 +"; + +const INIT_DONE: &str = r#"Initialization done! +You can start developing third-party Rustlings exercises in the `rustlings` directory :D + +If the initialization was done in a Rust project which is a Cargo workspace, you need to add the +path to the `rustlings` directory to the `workspace.exclude` list in the project's `Cargo.toml` +file. For example: + +[workspace] +exclude = ["rustlings"]"#; diff --git a/src/init.rs b/src/init.rs index 459519de..32020171 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,14 +1,14 @@ use anyhow::{bail, Context, Result}; use std::{ env::set_current_dir, - fs::{create_dir, OpenOptions}, - io::{self, ErrorKind, Write}, + fs::{self, create_dir}, + io::ErrorKind, path::Path, }; use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; -fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { +fn cargo_toml(exercise_infos: &[ExerciseInfo]) -> Vec { let mut cargo_toml = Vec::with_capacity(1 << 13); cargo_toml.extend_from_slice(b"bin = [\n"); for exercise_info in exercise_infos { @@ -23,39 +23,10 @@ fn create_cargo_toml(exercise_infos: &[ExerciseInfo]) -> io::Result<()> { cargo_toml.extend_from_slice(b".rs\" },\n"); } - cargo_toml.extend_from_slice( - br#"] + cargo_toml.extend_from_slice(b"]\n\n"); + cargo_toml.extend_from_slice(CARGO_TOML_PACKAGE.as_bytes()); -[package] -name = "rustlings" -edition = "2021" -publish = false -"#, - ); - OpenOptions::new() - .create_new(true) - .write(true) - .open("Cargo.toml")? - .write_all(&cargo_toml) -} - -fn create_gitignore() -> io::Result<()> { - OpenOptions::new() - .create_new(true) - .write(true) - .open(".gitignore")? - .write_all(GITIGNORE) -} - -fn create_vscode_dir() -> Result<()> { - create_dir(".vscode").context("Failed to create the directory `.vscode`")?; - OpenOptions::new() - .create_new(true) - .write(true) - .open(".vscode/extensions.json")? - .write_all(VS_CODE_EXTENSIONS_JSON)?; - - Ok(()) + cargo_toml } pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { @@ -78,21 +49,31 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { .init_exercises_dir() .context("Failed to initialize the `rustlings/exercises` directory")?; - create_cargo_toml(exercise_infos) + fs::write("Cargo.toml", cargo_toml(exercise_infos)) .context("Failed to create the file `rustlings/Cargo.toml`")?; - create_gitignore().context("Failed to create the file `rustlings/.gitignore`")?; + fs::write(".gitignore", GITIGNORE) + .context("Failed to create the file `rustlings/.gitignore`")?; - create_vscode_dir().context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + create_dir(".vscode").context("Failed to create the directory `rustlings/.vscode`")?; + fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) + .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; Ok(()) } -const GITIGNORE: &[u8] = b"/target -/.rustlings-state.txt +pub const CARGO_TOML_PACKAGE: &str = r#"[package] +name = "rustlings" +edition = "2021" +publish = false +"#; + +pub const GITIGNORE: &[u8] = b"Cargo.lock +.rustlings-state.txt +target "; -const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; +pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str = "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist From df448c069cae593d5eec37aa5b07c8103ae0f9b9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:15:14 +0200 Subject: [PATCH 150/433] Fix running dev commands --- src/init.rs | 7 +++++++ src/main.rs | 20 ++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/init.rs b/src/init.rs index 32020171..76482024 100644 --- a/src/init.rs +++ b/src/init.rs @@ -59,6 +59,8 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + println!("{POST_INIT_MSG}"); + Ok(()) } @@ -87,3 +89,8 @@ const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; + +const POST_INIT_MSG: &str = "Done initialization! + +Run `cd rustlings` to go into the generated directory. +Then run `rustlings` to get started."; diff --git a/src/main.rs b/src/main.rs index 7d4d8a5c..5188ee12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,10 +72,12 @@ fn main() -> Result<()> { bail!(FORMAT_VERSION_HIGHER_ERR); } - if matches!(args.command, Some(Subcommands::Init)) { - init::init(&info_file.exercises).context("Initialization failed")?; - println!("{POST_INIT_MSG}"); - return Ok(()); + match args.command { + Some(Subcommands::Init) => { + return init::init(&info_file.exercises).context("Initialization failed"); + } + Some(Subcommands::Dev(dev_command)) => return dev_command.run(), + _ => (), } if !Path::new("exercises").is_dir() { @@ -133,8 +135,6 @@ fn main() -> Result<()> { } } } - // `Init` is handled above. - Some(Subcommands::Init) => (), Some(Subcommands::Run { name }) => { if let Some(name) = name { app_state.set_current_exercise_by_name(&name)?; @@ -152,7 +152,8 @@ fn main() -> Result<()> { app_state.set_current_exercise_by_name(&name)?; println!("{}", app_state.current_exercise().hint); } - Some(Subcommands::Dev(dev_command)) => dev_command.run()?, + // Handled in an earlier match. + Some(Subcommands::Init | Subcommands::Dev(_)) => (), } Ok(()) @@ -167,11 +168,6 @@ const FORMAT_VERSION_HIGHER_ERR: &str = It is possible that you have an outdated version of Rustlings. Try to install the latest Rustlings version first."; -const POST_INIT_MSG: &str = "Done initialization! - -Run `cd rustlings` to go into the generated directory. -Then run `rustlings` to get started."; - const PRE_INIT_MSG: &str = r" Welcome to... _ _ _ From c07cf5bffe43402ced908dc315e5b8ee3e52bdcc Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:18:06 +0200 Subject: [PATCH 151/433] Fix typo --- src/dev/init.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev/init.rs b/src/dev/init.rs index d3821362..73c2c203 100644 --- a/src/dev/init.rs +++ b/src/dev/init.rs @@ -20,7 +20,7 @@ pub fn init() -> Result<()> { .context("Failed to create the file `rustlings/info.toml`")?; fs::write( - "rustligns/Cargo.toml", + "rustlings/Cargo.toml", format!("{CARGO_TOML_COMMENT}{}", crate::init::CARGO_TOML_PACKAGE), ) .context("Failed to create the file `rustlings/Cargo.toml`")?; @@ -32,7 +32,7 @@ pub fn init() -> Result<()> { .context("Failed to create the file `rustlings/README.md`")?; create_dir("rustlings/.vscode") - .context("Failed to create the directory `rustligns/.vscode`")?; + .context("Failed to create the directory `rustlings/.vscode`")?; fs::write( "rustlings/.vscode/extensions.json", crate::init::VS_CODE_EXTENSIONS_JSON, From d1ebbaa6f610ec492422806beb34e0dc7e4fc466 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:18:22 +0200 Subject: [PATCH 152/433] Add format_version to test info.toml files --- tests/fixture/failure/info.toml | 2 ++ tests/fixture/state/info.toml | 2 ++ tests/fixture/success/info.toml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml index 94ec6ead..ef99a07a 100644 --- a/tests/fixture/failure/info.toml +++ b/tests/fixture/failure/info.toml @@ -1,3 +1,5 @@ +format_version = 1 + [[exercises]] name = "compFailure" mode = "run" diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml index e5c4d8f4..eec24ea1 100644 --- a/tests/fixture/state/info.toml +++ b/tests/fixture/state/info.toml @@ -1,3 +1,5 @@ +format_version = 1 + [[exercises]] name = "pending_exercise" mode = "run" diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml index 674ba264..88650ecb 100644 --- a/tests/fixture/success/info.toml +++ b/tests/fixture/success/info.toml @@ -1,3 +1,5 @@ +format_version = 1 + [[exercises]] name = "compSuccess" mode = "run" From aa813fbce1305bb1beac88bff47f4279948cb3ac Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:30:28 +0200 Subject: [PATCH 153/433] Update Cargo.toml on `dev check` --- src/dev.rs | 6 ++++-- src/dev/check.rs | 17 ++++++++++++++--- src/init.rs | 2 +- src/main.rs | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index e09996f8..7905f387 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,6 +1,8 @@ use anyhow::{Context, Result}; use clap::Subcommand; +use crate::info_file::InfoFile; + mod check; mod init; @@ -11,10 +13,10 @@ pub enum DevCommands { } impl DevCommands { - pub fn run(self) -> Result<()> { + pub fn run(self, info_file: InfoFile) -> Result<()> { match self { DevCommands::Init => init::init().context(INIT_ERR), - DevCommands::Check => check::check(), + DevCommands::Check => check::check(info_file), } } } diff --git a/src/dev/check.rs b/src/dev/check.rs index 46d3ffec..9ae066bf 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,5 +1,16 @@ -use anyhow::Result; +use std::fs; -pub fn check() -> Result<()> { - todo!() +use anyhow::{Context, Result}; + +use crate::{info_file::InfoFile, init::cargo_toml}; + +pub fn check(info_file: InfoFile) -> Result<()> { + // TODO: Add checks + + fs::write("Cargo.toml", cargo_toml(&info_file.exercises)) + .context("Failed to update the file `Cargo.toml`")?; + + println!("Everything looks fine!"); + + Ok(()) } diff --git a/src/init.rs b/src/init.rs index 76482024..5fa44d40 100644 --- a/src/init.rs +++ b/src/init.rs @@ -8,7 +8,7 @@ use std::{ use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; -fn cargo_toml(exercise_infos: &[ExerciseInfo]) -> Vec { +pub fn cargo_toml(exercise_infos: &[ExerciseInfo]) -> Vec { let mut cargo_toml = Vec::with_capacity(1 << 13); cargo_toml.extend_from_slice(b"bin = [\n"); for exercise_info in exercise_infos { diff --git a/src/main.rs b/src/main.rs index 5188ee12..8b3f28f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,7 +76,7 @@ fn main() -> Result<()> { Some(Subcommands::Init) => { return init::init(&info_file.exercises).context("Initialization failed"); } - Some(Subcommands::Dev(dev_command)) => return dev_command.run(), + Some(Subcommands::Dev(dev_command)) => return dev_command.run(info_file), _ => (), } From 6566c5904f9463607eac15fd0072b551fac589dd Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:35:23 +0200 Subject: [PATCH 154/433] Tell about updating Cargo.toml --- src/dev/check.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 9ae066bf..1ee717b5 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -9,8 +9,9 @@ pub fn check(info_file: InfoFile) -> Result<()> { fs::write("Cargo.toml", cargo_toml(&info_file.exercises)) .context("Failed to update the file `Cargo.toml`")?; + println!("Updated `Cargo.toml`"); - println!("Everything looks fine!"); + println!("\nEverything looks fine!"); Ok(()) } From 87db9129bc361b88e206d1b27cb9056d2c8b00f1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:37:58 +0200 Subject: [PATCH 155/433] Add the mode field --- src/dev/init.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dev/init.rs b/src/dev/init.rs index 73c2c203..7dd3a3e1 100644 --- a/src/dev/init.rs +++ b/src/dev/init.rs @@ -71,6 +71,11 @@ name = "???" # Otherwise, the path is `exercises/NAME.rs` # dir = "???" +# The mode to run the exercise in. +# The mode "test" (preferred) runs the exercise's tests. +# The mode "run" only checks if the exercise compiles and runs it. +mode = "test" + # A mutli-line hint to be shown to users on request. hint = """???""" "#; From 86d716cf8a59092ba2078f2b0d80f95e155f2d64 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:43:34 +0200 Subject: [PATCH 156/433] Add comment about keeping dependencies --- src/dev/check.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/check.rs b/src/dev/check.rs index 1ee717b5..5910a75a 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -7,6 +7,7 @@ use crate::{info_file::InfoFile, init::cargo_toml}; pub fn check(info_file: InfoFile) -> Result<()> { // TODO: Add checks + // TODO: Keep dependencies! fs::write("Cargo.toml", cargo_toml(&info_file.exercises)) .context("Failed to update the file `Cargo.toml`")?; println!("Updated `Cargo.toml`"); From 4d9eb35ad7f6625da7e2a83afbb5c0bcee6ca96a Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:46:04 +0200 Subject: [PATCH 157/433] Prepare for publishing the first alpha version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index efc14419..fd82478e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ members = [ ] [workspace.package] -version = "6.0.0" +version = "6.0.0-alpha.0" authors = [ "Liv ", "Carol (Nichols || Goulding) ", @@ -40,7 +40,7 @@ crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros" } +rustlings-macros = "6.0.0-alpha.0" serde.workspace = true toml_edit.workspace = true which = "6.0.1" From 932f6b53a911cd549195be306fcbd65f5770f9d5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:47:09 +0200 Subject: [PATCH 158/433] Add myself to the list of authors :) --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index fd82478e..81e91ad4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ version = "6.0.0-alpha.0" authors = [ "Liv ", "Carol (Nichols || Goulding) ", + "Mo ", ] license = "MIT" edition = "2021" From f9be652b3b84dfa16265ec5804c267c66d381da5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 03:56:08 +0200 Subject: [PATCH 159/433] Ready to publish --- Cargo.lock | 4 ++-- Cargo.toml | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index defdacd4..65d22162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,7 +678,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.0.0" +version = "6.0.0-alpha.0" dependencies = [ "anyhow", "assert_cmd", @@ -696,7 +696,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0" +version = "6.0.0-alpha.0" dependencies = [ "quote", ] diff --git a/Cargo.toml b/Cargo.toml index 81e91ad4..06fbbf70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,13 @@ version.workspace = true authors.workspace = true license.workspace = true edition.workspace = true +include = [ + "/exercises/", + "/info.toml", + "/LICENSE", + "/README.md", + "/src/", +] [dependencies] anyhow.workspace = true @@ -41,7 +48,7 @@ crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" -rustlings-macros = "6.0.0-alpha.0" +rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } serde.workspace = true toml_edit.workspace = true which = "6.0.1" From 0ac5aa7af2dbff6f9d5e7cc712f409a86ea4cad0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 04:00:42 +0200 Subject: [PATCH 160/433] Fix typo --- src/dev/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/init.rs b/src/dev/init.rs index 7dd3a3e1..09935220 100644 --- a/src/dev/init.rs +++ b/src/dev/init.rs @@ -76,7 +76,7 @@ name = "???" # The mode "run" only checks if the exercise compiles and runs it. mode = "test" -# A mutli-line hint to be shown to users on request. +# A multi-line hint to be shown to users on request. hint = """???""" "#; From d322bcfcec8bd39a66fb5e6c0390e648e060b67c Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 04:04:45 +0200 Subject: [PATCH 161/433] Add description --- rustlings-macros/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 79279f57..095deaa3 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "rustlings-macros" +description = "A macros crate intended to be only used by rustlings" version.workspace = true authors.workspace = true license.workspace = true From 30636e7cf345757f95235744ff81376ae81c9aa2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 16 Apr 2024 21:46:07 +0200 Subject: [PATCH 162/433] Use colors inside the test --- src/exercise.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 2ec8d979..8bdf399f 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -69,7 +69,17 @@ impl Exercise { pub fn run(&self) -> Result { match self.mode { Mode::Run => self.cargo_cmd("run", &[]), - Mode::Test => self.cargo_cmd("test", &["--", "--nocapture", "--format", "pretty"]), + Mode::Test => self.cargo_cmd( + "test", + &[ + "--", + "--color", + "always", + "--nocapture", + "--format", + "pretty", + ], + ), Mode::Clippy => self.cargo_cmd( "clippy", &["--", "-D", "warnings", "-D", "clippy::float_cmp"], From 501b973c258a3c2e3a463d58c16402302184380f Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 15:55:50 +0200 Subject: [PATCH 163/433] Add "dev update" --- Cargo.lock | 9 ---- Cargo.toml | 14 ++---- dev/Cargo.toml | 4 +- gen-dev-cargo-toml/Cargo.toml | 10 ----- gen-dev-cargo-toml/src/main.rs | 68 ---------------------------- src/dev.rs | 9 ++-- src/dev/check.rs | 81 +++++++++++++++++++++++++++++++--- src/dev/init.rs | 23 +++++----- src/dev/update.rs | 53 ++++++++++++++++++++++ src/exercise.rs | 5 +-- src/init.rs | 39 +++++----------- src/main.rs | 27 ++++++++---- tests/dev_cargo_bins.rs | 44 ------------------ 13 files changed, 181 insertions(+), 205 deletions(-) delete mode 100644 gen-dev-cargo-toml/Cargo.toml delete mode 100644 gen-dev-cargo-toml/src/main.rs create mode 100644 src/dev/update.rs delete mode 100644 tests/dev_cargo_bins.rs diff --git a/Cargo.lock b/Cargo.lock index 65d22162..4c95ca85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,15 +311,6 @@ dependencies = [ "libc", ] -[[package]] -name = "gen-dev-cargo-toml" -version = "0.0.0" -dependencies = [ - "anyhow", - "serde", - "toml_edit", -] - [[package]] name = "hashbrown" version = "0.14.3" diff --git a/Cargo.toml b/Cargo.toml index 06fbbf70..cde41823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,6 @@ exclude = [ "tests/fixture/success", "dev", ] -members = [ - "gen-dev-cargo-toml", -] [workspace.package] version = "6.0.0-alpha.0" @@ -20,11 +17,6 @@ authors = [ license = "MIT" edition = "2021" -[workspace.dependencies] -anyhow = "1.0.82" -serde = { version = "1.0.197", features = ["derive"] } -toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } - [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" @@ -42,15 +34,15 @@ include = [ ] [dependencies] -anyhow.workspace = true +anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } -serde.workspace = true -toml_edit.workspace = true +serde = { version = "1.0.197", features = ["derive"] } +toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } which = "6.0.1" [dev-dependencies] diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 1d230ebb..e66973e0 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,6 +1,4 @@ -# This file is a hack to allow using `cargo run` to test `rustlings` during development. -# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`. - +# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update` bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, diff --git a/gen-dev-cargo-toml/Cargo.toml b/gen-dev-cargo-toml/Cargo.toml deleted file mode 100644 index 8922ae8c..00000000 --- a/gen-dev-cargo-toml/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "gen-dev-cargo-toml" -publish = false -license.workspace = true -edition.workspace = true - -[dependencies] -anyhow.workspace = true -serde.workspace = true -toml_edit.workspace = true diff --git a/gen-dev-cargo-toml/src/main.rs b/gen-dev-cargo-toml/src/main.rs deleted file mode 100644 index 43b4ebd7..00000000 --- a/gen-dev-cargo-toml/src/main.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Generates `dev/Cargo.toml` such that it is synced with `info.toml`. -// `dev/Cargo.toml` is a hack to allow using `cargo run` to test `rustlings` -// during development. - -use anyhow::{bail, Context, Result}; -use serde::Deserialize; -use std::{ - fs::{self, create_dir}, - io::ErrorKind, -}; - -#[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: Option, -} - -#[derive(Deserialize)] -struct InfoFile { - exercises: Vec, -} - -fn main() -> Result<()> { - let exercise_infos = toml_edit::de::from_str::( - &fs::read_to_string("info.toml").context("Failed to read `info.toml`")?, - ) - .context("Failed to deserialize `info.toml`")? - .exercises; - - let mut buf = Vec::with_capacity(1 << 14); - - buf.extend_from_slice( - b"# This file is a hack to allow using `cargo run` to test `rustlings` during development. -# You shouldn't edit it manually. It is created and updated by running `cargo run -p gen-dev-cargo-toml`. - -bin = [\n", - ); - - for exercise_info in exercise_infos { - buf.extend_from_slice(b" { name = \""); - buf.extend_from_slice(exercise_info.name.as_bytes()); - buf.extend_from_slice(b"\", path = \"../exercises/"); - if let Some(dir) = &exercise_info.dir { - buf.extend_from_slice(dir.as_bytes()); - buf.push(b'/'); - } - buf.extend_from_slice(exercise_info.name.as_bytes()); - buf.extend_from_slice(b".rs\" },\n"); - } - - buf.extend_from_slice( - br#"] - -[package] -name = "rustlings-dev" -edition = "2021" -publish = false -"#, - ); - - if let Err(e) = create_dir("dev") { - if e.kind() != ErrorKind::AlreadyExists { - bail!("Failed to create the `dev` directory: {e}"); - } - } - - fs::write("dev/Cargo.toml", buf).context("Failed to write `dev/Cargo.toml`") -} diff --git a/src/dev.rs b/src/dev.rs index 7905f387..feca99e5 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,22 +1,23 @@ use anyhow::{Context, Result}; use clap::Subcommand; -use crate::info_file::InfoFile; - mod check; mod init; +mod update; #[derive(Subcommand)] pub enum DevCommands { Init, Check, + Update, } impl DevCommands { - pub fn run(self, info_file: InfoFile) -> Result<()> { + pub fn run(self) -> Result<()> { match self { DevCommands::Init => init::init().context(INIT_ERR), - DevCommands::Check => check::check(info_file), + DevCommands::Check => check::check(), + DevCommands::Update => update::update(), } } } diff --git a/src/dev/check.rs b/src/dev/check.rs index 5910a75a..bc8e4592 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,16 +1,83 @@ +use anyhow::{bail, Context, Result}; use std::fs; -use anyhow::{Context, Result}; +use crate::{ + info_file::{ExerciseInfo, InfoFile}, + DEVELOPING_OFFIFICAL_RUSTLINGS, +}; -use crate::{info_file::InfoFile, init::cargo_toml}; +pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { + let start_ind = cargo_toml + .find("bin = [") + .context("Failed to find the start of the `bin` list (`bin = [`)")? + + 7; + let end_ind = start_ind + + cargo_toml + .get(start_ind..) + .and_then(|slice| slice.as_bytes().iter().position(|c| *c == b']')) + .context("Failed to find the end of the `bin` list (`]`)")?; + + Ok((start_ind, end_ind)) +} + +pub fn append_bins( + buf: &mut Vec, + exercise_infos: &[ExerciseInfo], + exercise_path_prefix: &[u8], +) { + buf.push(b'\n'); + for exercise_info in exercise_infos { + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"\", path = \""); + buf.extend_from_slice(exercise_path_prefix); + buf.extend_from_slice(b"exercises/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.push(b'/'); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); + } +} + +fn check_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; + let mut new_bins = Vec::with_capacity(1 << 13); + append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); + + if old_bins != new_bins { + bail!("`Cargo.toml` is outdated. Run `rustlings dev update` to update it"); + } + + Ok(()) +} + +pub fn check() -> Result<()> { + let info_file = InfoFile::parse()?; -pub fn check(info_file: InfoFile) -> Result<()> { // TODO: Add checks - // TODO: Keep dependencies! - fs::write("Cargo.toml", cargo_toml(&info_file.exercises)) - .context("Failed to update the file `Cargo.toml`")?; - println!("Updated `Cargo.toml`"); + if DEVELOPING_OFFIFICAL_RUSTLINGS { + check_cargo_toml( + &info_file.exercises, + include_str!("../../dev/Cargo.toml"), + b"../", + ) + .context("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it")?; + } else { + let current_cargo_toml = + fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; + check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"").context( + "The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it", + )?; + } println!("\nEverything looks fine!"); diff --git a/src/dev/init.rs b/src/dev/init.rs index 09935220..3ce5055c 100644 --- a/src/dev/init.rs +++ b/src/dev/init.rs @@ -1,6 +1,5 @@ -use std::fs::{self, create_dir}; - use anyhow::{Context, Result}; +use std::fs::{self, create_dir}; use crate::CURRENT_FORMAT_VERSION; @@ -19,11 +18,8 @@ pub fn init() -> Result<()> { ) .context("Failed to create the file `rustlings/info.toml`")?; - fs::write( - "rustlings/Cargo.toml", - format!("{CARGO_TOML_COMMENT}{}", crate::init::CARGO_TOML_PACKAGE), - ) - .context("Failed to create the file `rustlings/Cargo.toml`")?; + fs::write("rustlings/Cargo.toml", CARGO_TOML) + .context("Failed to create the file `rustlings/Cargo.toml`")?; fs::write("rustlings/.gitignore", crate::init::GITIGNORE) .context("Failed to create the file `rustlings/.gitignore`")?; @@ -80,10 +76,17 @@ mode = "test" hint = """???""" "#; -const CARGO_TOML_COMMENT: &str = - "# You shouldn't edit this file manually! It is updated by `rustlings dev check` +const CARGO_TOML: &[u8] = + br#"# Don't edit the `bin` list manually! It is updated by `rustlings dev update` +bin = [] -"; +[package] +name = "rustlings" +edition = "2021" +publish = false + +[dependencies] +"#; const README: &str = "# Rustlings 🦀 diff --git a/src/dev/update.rs b/src/dev/update.rs new file mode 100644 index 00000000..981934d6 --- /dev/null +++ b/src/dev/update.rs @@ -0,0 +1,53 @@ +use std::fs; + +use anyhow::{Context, Result}; + +use crate::{ + info_file::{ExerciseInfo, InfoFile}, + DEVELOPING_OFFIFICAL_RUSTLINGS, +}; + +use super::check::{append_bins, bins_start_end_ind}; + +fn update_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + cargo_toml_path: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let mut new_cargo_toml = Vec::with_capacity(1 << 13); + new_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); + append_bins(&mut new_cargo_toml, exercise_infos, exercise_path_prefix); + new_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes()); + + fs::write(cargo_toml_path, new_cargo_toml).context("Failed to write the `Cargo.toml` file")?; + + Ok(()) +} + +pub fn update() -> Result<()> { + let info_file = InfoFile::parse()?; + + if DEVELOPING_OFFIFICAL_RUSTLINGS { + update_cargo_toml( + &info_file.exercises, + include_str!("../../dev/Cargo.toml"), + "dev/Cargo.toml", + b"../", + ) + .context("Failed to update the file `dev/Cargo.toml`")?; + + println!("Updated `dev/Cargo.toml`"); + } else { + let current_cargo_toml = + fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; + update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, "Cargo.toml", b"") + .context("Failed to update the file `Cargo.toml`")?; + + println!("Updated `Cargo.toml`"); + } + + Ok(()) +} diff --git a/src/exercise.rs b/src/exercise.rs index 8bdf399f..c4df9999 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -9,6 +9,7 @@ use std::{ use crate::{ embedded::{WriteStrategy, EMBEDDED_FILES}, info_file::Mode, + DEVELOPING_OFFIFICAL_RUSTLINGS, }; pub struct TerminalFileLink<'a> { @@ -50,9 +51,7 @@ impl Exercise { cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - // Use `dev/Cargo.toml` when in the directory of the repository. - #[cfg(debug_assertions)] - if std::path::Path::new("tests").exists() { + if DEVELOPING_OFFIFICAL_RUSTLINGS { cmd.arg("--manifest-path").arg("dev/Cargo.toml"); } diff --git a/src/init.rs b/src/init.rs index 5fa44d40..52315e24 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,30 +6,19 @@ use std::{ path::Path, }; -use crate::{embedded::EMBEDDED_FILES, info_file::ExerciseInfo}; +use crate::embedded::EMBEDDED_FILES; -pub fn cargo_toml(exercise_infos: &[ExerciseInfo]) -> Vec { - let mut cargo_toml = Vec::with_capacity(1 << 13); - cargo_toml.extend_from_slice(b"bin = [\n"); - for exercise_info in exercise_infos { - cargo_toml.extend_from_slice(b" { name = \""); - cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); - cargo_toml.extend_from_slice(b"\", path = \"exercises/"); - if let Some(dir) = &exercise_info.dir { - cargo_toml.extend_from_slice(dir.as_bytes()); - cargo_toml.push(b'/'); - } - cargo_toml.extend_from_slice(exercise_info.name.as_bytes()); - cargo_toml.extend_from_slice(b".rs\" },\n"); +const CARGO_TOML: &[u8] = { + let cargo_toml = include_bytes!("../dev/Cargo.toml"); + // Skip the first line (comment). + let mut start_ind = 0; + while cargo_toml[start_ind] != b'\n' { + start_ind += 1; } + cargo_toml.split_at(start_ind + 1).1 +}; - cargo_toml.extend_from_slice(b"]\n\n"); - cargo_toml.extend_from_slice(CARGO_TOML_PACKAGE.as_bytes()); - - cargo_toml -} - -pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { +pub fn init() -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); } @@ -49,7 +38,7 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { .init_exercises_dir() .context("Failed to initialize the `rustlings/exercises` directory")?; - fs::write("Cargo.toml", cargo_toml(exercise_infos)) + fs::write("Cargo.toml", CARGO_TOML) .context("Failed to create the file `rustlings/Cargo.toml`")?; fs::write(".gitignore", GITIGNORE) @@ -64,12 +53,6 @@ pub fn init(exercise_infos: &[ExerciseInfo]) -> Result<()> { Ok(()) } -pub const CARGO_TOML_PACKAGE: &str = r#"[package] -name = "rustlings" -edition = "2021" -publish = false -"#; - pub const GITIGNORE: &[u8] = b"Cargo.lock .rustlings-state.txt target diff --git a/src/main.rs b/src/main.rs index 8b3f28f9..ea5f7c92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,17 @@ mod watch; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; const CURRENT_FORMAT_VERSION: u8 = 1; +const DEVELOPING_OFFIFICAL_RUSTLINGS: bool = { + #[allow(unused_assignments, unused_mut)] + let mut debug_profile = false; + + #[cfg(debug_assertions)] + { + debug_profile = true; + } + + debug_profile +}; /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] @@ -66,17 +77,11 @@ fn main() -> Result<()> { which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; - let info_file = InfoFile::parse()?; - - if info_file.format_version > CURRENT_FORMAT_VERSION { - bail!(FORMAT_VERSION_HIGHER_ERR); - } - match args.command { Some(Subcommands::Init) => { - return init::init(&info_file.exercises).context("Initialization failed"); + return init::init().context("Initialization failed"); } - Some(Subcommands::Dev(dev_command)) => return dev_command.run(info_file), + Some(Subcommands::Dev(dev_command)) => return dev_command.run(), _ => (), } @@ -85,6 +90,12 @@ fn main() -> Result<()> { exit(1); } + let info_file = InfoFile::parse()?; + + if info_file.format_version > CURRENT_FORMAT_VERSION { + bail!(FORMAT_VERSION_HIGHER_ERR); + } + let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), diff --git a/tests/dev_cargo_bins.rs b/tests/dev_cargo_bins.rs deleted file mode 100644 index 81f48b1e..00000000 --- a/tests/dev_cargo_bins.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Makes sure that `dev/Cargo.toml` is synced with `info.toml`. -// When this test fails, you just need to run `cargo run -p gen-dev-cargo-toml`. - -use serde::Deserialize; -use std::fs; - -#[derive(Deserialize)] -struct ExerciseInfo { - name: String, - dir: Option, -} - -#[derive(Deserialize)] -struct InfoFile { - exercises: Vec, -} - -#[test] -fn dev_cargo_bins() { - let cargo_toml = fs::read_to_string("dev/Cargo.toml").unwrap(); - - let exercise_infos = - toml_edit::de::from_str::(&fs::read_to_string("info.toml").unwrap()) - .unwrap() - .exercises; - - let mut start_ind = 0; - for exercise_info in exercise_infos { - let name_start = start_ind + cargo_toml[start_ind..].find('"').unwrap() + 1; - let name_end = name_start + cargo_toml[name_start..].find('"').unwrap(); - assert_eq!(exercise_info.name, &cargo_toml[name_start..name_end]); - - let path_start = name_end + cargo_toml[name_end + 1..].find('"').unwrap() + 2; - let path_end = path_start + cargo_toml[path_start..].find('"').unwrap(); - let expected_path = if let Some(dir) = exercise_info.dir { - format!("../exercises/{dir}/{}.rs", exercise_info.name) - } else { - format!("../exercises/{}.rs", exercise_info.name) - }; - assert_eq!(expected_path, &cargo_toml[path_start..path_end]); - - start_ind = path_end + 1; - } -} From a2506f154b929b74e7d5c286cee6eabb008aa1fd Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 15:56:24 +0200 Subject: [PATCH 164/433] Update serde --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c95ca85..b194de61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,9 +580,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -721,18 +721,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.198" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index cde41823..5e3be113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } -serde = { version = "1.0.197", features = ["derive"] } +serde = { version = "1.0.198", features = ["derive"] } toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } which = "6.0.1" From 7f433ae28f0e79c62f53b74a14042f916cb13650 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 16:09:25 +0200 Subject: [PATCH 165/433] Check the format version in `dev check` --- src/dev/check.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index bc8e4592..daf5bdb2 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,9 +1,9 @@ use anyhow::{bail, Context, Result}; -use std::fs; +use std::{cmp::Ordering, fs}; use crate::{ info_file::{ExerciseInfo, InfoFile}, - DEVELOPING_OFFIFICAL_RUSTLINGS, + CURRENT_FORMAT_VERSION, DEVELOPING_OFFIFICAL_RUSTLINGS, }; pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { @@ -62,7 +62,11 @@ fn check_cargo_toml( pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; - // TODO: Add checks + match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { + Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), + Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), + Ordering::Equal => (), + } if DEVELOPING_OFFIFICAL_RUSTLINGS { check_cargo_toml( From 7005d8a400ce2a61f05bae1f71e144e0a25a9bf0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 16:11:44 +0200 Subject: [PATCH 166/433] Fix typo --- src/dev/check.rs | 4 ++-- src/dev/update.rs | 4 ++-- src/exercise.rs | 4 ++-- src/main.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index daf5bdb2..9002eaff 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -3,7 +3,7 @@ use std::{cmp::Ordering, fs}; use crate::{ info_file::{ExerciseInfo, InfoFile}, - CURRENT_FORMAT_VERSION, DEVELOPING_OFFIFICAL_RUSTLINGS, + CURRENT_FORMAT_VERSION, DEVELOPING_OFFICIAL_RUSTLINGS, }; pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { @@ -68,7 +68,7 @@ pub fn check() -> Result<()> { Ordering::Equal => (), } - if DEVELOPING_OFFIFICAL_RUSTLINGS { + if DEVELOPING_OFFICIAL_RUSTLINGS { check_cargo_toml( &info_file.exercises, include_str!("../../dev/Cargo.toml"), diff --git a/src/dev/update.rs b/src/dev/update.rs index 981934d6..65dcf768 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use crate::{ info_file::{ExerciseInfo, InfoFile}, - DEVELOPING_OFFIFICAL_RUSTLINGS, + DEVELOPING_OFFICIAL_RUSTLINGS, }; use super::check::{append_bins, bins_start_end_ind}; @@ -30,7 +30,7 @@ fn update_cargo_toml( pub fn update() -> Result<()> { let info_file = InfoFile::parse()?; - if DEVELOPING_OFFIFICAL_RUSTLINGS { + if DEVELOPING_OFFICIAL_RUSTLINGS { update_cargo_toml( &info_file.exercises, include_str!("../../dev/Cargo.toml"), diff --git a/src/exercise.rs b/src/exercise.rs index c4df9999..60a65bb6 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -9,7 +9,7 @@ use std::{ use crate::{ embedded::{WriteStrategy, EMBEDDED_FILES}, info_file::Mode, - DEVELOPING_OFFIFICAL_RUSTLINGS, + DEVELOPING_OFFICIAL_RUSTLINGS, }; pub struct TerminalFileLink<'a> { @@ -51,7 +51,7 @@ impl Exercise { cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - if DEVELOPING_OFFIFICAL_RUSTLINGS { + if DEVELOPING_OFFICIAL_RUSTLINGS { cmd.arg("--manifest-path").arg("dev/Cargo.toml"); } diff --git a/src/main.rs b/src/main.rs index ea5f7c92..fa5542af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ mod watch; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; const CURRENT_FORMAT_VERSION: u8 = 1; -const DEVELOPING_OFFIFICAL_RUSTLINGS: bool = { +const DEVELOPING_OFFICIAL_RUSTLINGS: bool = { #[allow(unused_assignments, unused_mut)] let mut debug_profile = false; From 28ec0f864a5d041e15c08049a600c1f0e4fefd2e Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 18:19:08 +0200 Subject: [PATCH 167/433] Check the info file --- src/dev/check.rs | 125 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 9002eaff..ac2c603d 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,11 +1,128 @@ use anyhow::{bail, Context, Result}; -use std::{cmp::Ordering, fs}; +use std::{ + cmp::Ordering, + fs::{self, read_dir}, + path::PathBuf, +}; use crate::{ info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, DEVELOPING_OFFICIAL_RUSTLINGS, }; +fn forbidden_char(input: &str) -> Option { + input.chars().find(|c| *c != '_' && !c.is_alphanumeric()) +} + +fn check_info_file_exercises(info_file: &InfoFile) -> Result> { + let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + for exercise_info in &info_file.exercises { + if let Some(c) = forbidden_char(&exercise_info.name) { + bail!( + "Char `{c}` in the exercise name `{}` is not allowed", + exercise_info.name, + ); + } + + if let Some(dir) = &exercise_info.dir { + if let Some(c) = forbidden_char(dir) { + bail!("Char `{c}` in the exercise dir `{dir}` is not allowed"); + } + } + + if !names.insert(exercise_info.name.as_str()) { + bail!( + "The exercise name {} is duplicated. Exercise names must all be unique", + exercise_info.name, + ); + } + + paths.insert(PathBuf::from(exercise_info.path())); + } + + Ok(paths) +} + +fn check_exercise_dir_files( + info_file: &InfoFile, + info_file_paths: hashbrown::HashSet, +) -> Result> { + let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { + let entry = entry.context("Failed to read the `exercises` directory")?; + + if entry.file_type().unwrap().is_file() { + let path = entry.path(); + let file_name = path.file_name().unwrap(); + if file_name == "README.md" { + continue; + } + + if !info_file_paths.contains(&path) { + bail!("`{}` is expected to be an exercise file corresponding to some exercise in `info.toml`", path.display()); + } + + let file_name = file_name.to_string_lossy(); + names.insert(file_name[..file_name.len() - 3].to_string()); + continue; + } + + let dir_path = entry.path(); + for entry in read_dir(&dir_path) + .with_context(|| format!("Failed to open the directory {}", dir_path.display()))? + { + let entry = entry + .with_context(|| format!("Failed to read the directory {}", dir_path.display()))?; + let path = entry.path(); + + if !entry.file_type().unwrap().is_file() { + bail!("Found {} but expected only files", path.display()); + } + + let file_name = path.file_name().unwrap(); + if file_name == "README.md" { + continue; + } + + if !info_file_paths.contains(&path) { + bail!("`{}` is expected to be an exercise file corresponding to some exercise in `info.toml`", path.display()); + } + + let file_name = file_name.to_string_lossy(); + names.insert(file_name[..file_name.len() - 3].to_string()); + } + } + + Ok(names) +} + +fn check_info_file(info_file: &InfoFile) -> Result<()> { + match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { + Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), + Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), + Ordering::Equal => (), + } + + let info_file_paths = check_info_file_exercises(info_file)?; + let names_in_exercises_dir = check_exercise_dir_files(info_file, info_file_paths)?; + + // Now, we know that every file has an exercise in `info.toml`. + // But we need to check that every exercise in `info.toml` has a file. + if names_in_exercises_dir.len() != info_file.exercises.len() { + for exercise_info in &info_file.exercises { + if !names_in_exercises_dir.contains(&exercise_info.name) { + bail!( + "No exercise file found for the exercise `{}`", + exercise_info.name, + ); + } + } + } + + Ok(()) +} + pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { let start_ind = cargo_toml .find("bin = [") @@ -62,11 +179,7 @@ fn check_cargo_toml( pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; - match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { - Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), - Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), - Ordering::Equal => (), - } + check_info_file(&info_file)?; if DEVELOPING_OFFICIAL_RUSTLINGS { check_cargo_toml( From b9167e9299bfe7c644cdbc2f6e933873f06b1c3f Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 18:19:15 +0200 Subject: [PATCH 168/433] Remove redundant checks --- rustlings-macros/src/lib.rs | 11 ++--------- src/info_file.rs | 8 -------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index d8da666e..d95a93a6 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -15,8 +15,8 @@ pub fn include_files(_: TokenStream) -> TokenStream { let mut files = Vec::with_capacity(8); let mut dirs = Vec::with_capacity(128); - for entry in read_dir("exercises").expect("Failed to open the exercises directory") { - let entry = entry.expect("Failed to read the exercises directory"); + for entry in read_dir("exercises").expect("Failed to open the `exercises` directory") { + let entry = entry.expect("Failed to read the `exercises` directory"); if entry.file_type().unwrap().is_file() { let path = entry.path(); @@ -46,13 +46,6 @@ pub fn include_files(_: TokenStream) -> TokenStream { return None; } - if path.extension() != Some("rs".as_ref()) { - panic!( - "Found {} but expected only README.md and .rs files", - path.display(), - ); - } - Some(path_to_string(path)) }); diff --git a/src/info_file.rs b/src/info_file.rs index 18e77b9f..879609e3 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -64,14 +64,6 @@ impl InfoFile { bail!("{NO_EXERCISES_ERR}"); } - let mut names_set = hashbrown::HashSet::with_capacity(slf.exercises.len()); - for exercise in &slf.exercises { - if !names_set.insert(exercise.name.as_str()) { - bail!("Exercise names must all be unique!") - } - } - drop(names_set); - Ok(slf) } } From d42a6e741564313460fceb055d0aebe599cbe232 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 18:59:40 +0200 Subject: [PATCH 169/433] Print the path of the missing file --- src/dev/check.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index ac2c603d..2c48f0e1 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -17,6 +17,7 @@ fn forbidden_char(input: &str) -> Option { fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + for exercise_info in &info_file.exercises { if let Some(c) = forbidden_char(&exercise_info.name) { bail!( @@ -49,6 +50,7 @@ fn check_exercise_dir_files( info_file_paths: hashbrown::HashSet, ) -> Result> { let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); + for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { let entry = entry.context("Failed to read the `exercises` directory")?; @@ -89,8 +91,11 @@ fn check_exercise_dir_files( bail!("`{}` is expected to be an exercise file corresponding to some exercise in `info.toml`", path.display()); } + // The file name must be valid Unicode with the `.rs` extension + // because it is part of the info file paths. let file_name = file_name.to_string_lossy(); - names.insert(file_name[..file_name.len() - 3].to_string()); + let file_name_without_rs_extension = file_name[..file_name.len() - 3].to_string(); + names.insert(file_name_without_rs_extension); } } @@ -112,10 +117,7 @@ fn check_info_file(info_file: &InfoFile) -> Result<()> { if names_in_exercises_dir.len() != info_file.exercises.len() { for exercise_info in &info_file.exercises { if !names_in_exercises_dir.contains(&exercise_info.name) { - bail!( - "No exercise file found for the exercise `{}`", - exercise_info.name, - ); + bail!("The file `{}` is missing", exercise_info.path()); } } } @@ -178,7 +180,6 @@ fn check_cargo_toml( pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; - check_info_file(&info_file)?; if DEVELOPING_OFFICIAL_RUSTLINGS { From d6bb27ec2060863c38794b7c2511ca7399e29172 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 19:12:10 +0200 Subject: [PATCH 170/433] Check for empty field values --- src/dev/check.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/dev/check.rs b/src/dev/check.rs index 2c48f0e1..3cb5345a 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -19,6 +19,9 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result Result Date: Wed, 17 Apr 2024 19:16:48 +0200 Subject: [PATCH 171/433] Trim before checking if the hint is empty --- src/dev/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 3cb5345a..4688e044 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -38,7 +38,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result Date: Wed, 17 Apr 2024 22:46:21 +0200 Subject: [PATCH 172/433] Require a main function in all exercises --- exercises/03_if/if1.rs | 4 + exercises/03_if/if2.rs | 4 + exercises/03_if/if3.rs | 4 + .../04_primitive_types/primitive_types4.rs | 4 + .../04_primitive_types/primitive_types6.rs | 4 + exercises/05_vecs/vecs1.rs | 4 + exercises/05_vecs/vecs2.rs | 4 + exercises/07_structs/structs1.rs | 4 + exercises/07_structs/structs2.rs | 4 + exercises/07_structs/structs3.rs | 4 + exercises/08_enums/enums3.rs | 4 + exercises/09_strings/strings3.rs | 4 + exercises/11_hashmaps/hashmaps1.rs | 4 + exercises/11_hashmaps/hashmaps2.rs | 6 +- exercises/11_hashmaps/hashmaps3.rs | 10 ++- exercises/12_options/options1.rs | 4 + exercises/12_options/options2.rs | 4 + exercises/13_error_handling/errors1.rs | 4 + exercises/13_error_handling/errors2.rs | 4 + exercises/13_error_handling/errors4.rs | 4 + exercises/13_error_handling/errors6.rs | 4 + exercises/14_generics/generics2.rs | 4 + exercises/15_traits/traits2.rs | 4 + exercises/15_traits/traits3.rs | 4 + exercises/15_traits/traits4.rs | 4 + exercises/17_tests/tests1.rs | 4 + exercises/17_tests/tests2.rs | 4 + exercises/17_tests/tests3.rs | 4 + exercises/17_tests/tests4.rs | 8 +- exercises/18_iterators/iterators2.rs | 4 + exercises/18_iterators/iterators3.rs | 4 + exercises/18_iterators/iterators4.rs | 4 + exercises/18_iterators/iterators5.rs | 4 + exercises/19_smart_pointers/cow1.rs | 4 + exercises/23_conversions/as_ref_mut.rs | 4 + exercises/quiz1.rs | 4 + exercises/quiz2.rs | 4 + exercises/quiz3.rs | 10 ++- src/dev/check.rs | 73 ++++++++++--------- 39 files changed, 199 insertions(+), 44 deletions(-) diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs index a1df66bb..dbd0d285 100644 --- a/exercises/03_if/if1.rs +++ b/exercises/03_if/if1.rs @@ -10,6 +10,10 @@ pub fn bigger(a: i32, b: i32) -> i32 { // - additional variables } +fn main() { + // You can optionally experiment here. +} + // Don't mind this for now :) #[cfg(test)] mod tests { diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index 7b9c05f6..a1ed5c8b 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -13,6 +13,10 @@ pub fn foo_if_fizz(fizzish: &str) -> &str { } } +fn main() { + // You can optionally experiment here. +} + // No test changes needed! #[cfg(test)] mod tests { diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index caba172b..0b44c5ab 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -27,6 +27,10 @@ pub fn animal_habitat(animal: &str) -> &'static str { habitat } +fn main() { + // You can optionally experiment here. +} + // No test changes needed. #[cfg(test)] mod tests { diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index 8ed0a82a..f99d8895 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -5,6 +5,10 @@ // Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand // for a hint. +fn main() { + // You can optionally experiment here. +} + #[test] fn slice_out_of_array() { let a = [1, 2, 3, 4, 5]; diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs index 5f82f10f..48e84d34 100644 --- a/exercises/04_primitive_types/primitive_types6.rs +++ b/exercises/04_primitive_types/primitive_types6.rs @@ -6,6 +6,10 @@ // Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand // for a hint. +fn main() { + // You can optionally experiment here. +} + #[test] fn indexing_tuple() { let numbers = (1, 2, 3); diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index c64acbbd..5f44cb24 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -14,6 +14,10 @@ fn array_and_vec() -> ([i32; 4], Vec) { (a, v) } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index d64d3d16..1b16f0b4 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -26,6 +26,10 @@ fn vec_map(v: &Vec) -> Vec { }).collect() } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs index 29781214..cd8b81c9 100644 --- a/exercises/07_structs/structs1.rs +++ b/exercises/07_structs/structs1.rs @@ -14,6 +14,10 @@ struct ColorTupleStruct(/* TODO: Something goes here */); #[derive(Debug)] struct UnitLikeStruct; +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs index a7a2deca..7e61e752 100644 --- a/exercises/07_structs/structs2.rs +++ b/exercises/07_structs/structs2.rs @@ -28,6 +28,10 @@ fn create_order_template() -> Order { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 9835b811..bd562a12 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -38,6 +38,10 @@ impl Package { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index 580a553e..56c04fe6 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -45,6 +45,10 @@ impl State { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index dedc081f..d53f654e 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -18,6 +18,10 @@ fn replace_me(input: &str) -> String { ??? } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs index 5a52f611..51146dfc 100644 --- a/exercises/11_hashmaps/hashmaps1.rs +++ b/exercises/11_hashmaps/hashmaps1.rs @@ -24,6 +24,10 @@ fn fruit_basket() -> HashMap { basket } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index 27306439..47983f6b 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -41,6 +41,10 @@ fn fruit_basket(basket: &mut HashMap) { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -79,7 +83,7 @@ mod tests { let count = basket.values().sum::(); assert!(count > 11); } - + #[test] fn all_fruit_types_in_basket() { let mut basket = get_fruit_basket(); diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 775a4014..33229096 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -5,9 +5,9 @@ // Example: England,France,4,2 (England scored 4 goals, France 2). // // You have to build a scores table containing the name of the team, the total -// number of goals the team scored, and the total number of goals the team -// conceded. One approach to build the scores table is to use a Hashmap. -// The solution is partially written to use a Hashmap, +// number of goals the team scored, and the total number of goals the team +// conceded. One approach to build the scores table is to use a Hashmap. +// The solution is partially written to use a Hashmap, // complete it to pass the test. // // Make me pass the tests! @@ -42,6 +42,10 @@ fn build_scores_table(results: String) -> HashMap { scores } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index ba4b1cda..aecb123a 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -14,6 +14,10 @@ fn maybe_icecream(time_of_day: u16) -> Option { ??? } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs index 73f707e3..d183d1d5 100644 --- a/exercises/12_options/options2.rs +++ b/exercises/12_options/options2.rs @@ -3,6 +3,10 @@ // Execute `rustlings hint options2` or use the `hint` watch subcommand for a // hint. +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { #[test] diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 9767f2c8..7991c420 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -9,6 +9,10 @@ // Execute `rustlings hint errors1` or use the `hint` watch subcommand for a // hint. +fn main() { + // You can optionally experiment here. +} + pub fn generate_nametag_text(name: String) -> Option { if name.is_empty() { // Empty names aren't allowed. diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index 88d1bf43..051516b9 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -29,6 +29,10 @@ pub fn total_cost(item_quantity: &str) -> Result { Ok(qty * cost_per_item + processing_fee) } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index 0e5c08bf..94494170 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -19,6 +19,10 @@ impl PositiveNonzeroInteger { } } +fn main() { + // You can optionally experiment here. +} + #[test] fn test_creation() { assert!(PositiveNonzeroInteger::new(10).is_ok()); diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index de73a9a5..363a3b91 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -54,6 +54,10 @@ impl PositiveNonzeroInteger { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod test { use super::*; diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index d50ed174..068468ba 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -16,6 +16,10 @@ impl Wrapper { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index 9a2bc07a..18ebcb08 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -14,6 +14,10 @@ trait AppendBar { // TODO: Implement trait `AppendBar` for a vector of strings. +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 357f1d7f..8412afa2 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -23,6 +23,10 @@ struct OtherSoftware { impl Licensed for SomeSoftware {} // Don't edit this line impl Licensed for OtherSoftware {} // Don't edit this line +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index 7242c483..18db0d67 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -25,6 +25,10 @@ fn compare_license_types(software: ??, software_two: ??) -> bool { software.licensing_info() == software_two.licensing_info() } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs index bde21083..d32ace1e 100644 --- a/exercises/17_tests/tests1.rs +++ b/exercises/17_tests/tests1.rs @@ -10,6 +10,10 @@ // Execute `rustlings hint tests1` or use the `hint` watch subcommand for a // hint. +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { #[test] diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs index aea5c0e4..501c44bc 100644 --- a/exercises/17_tests/tests2.rs +++ b/exercises/17_tests/tests2.rs @@ -6,6 +6,10 @@ // Execute `rustlings hint tests2` or use the `hint` watch subcommand for a // hint. +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { #[test] diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index d815e058..a2093cf2 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -11,6 +11,10 @@ pub fn is_even(num: i32) -> bool { num % 2 == 0 } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs index 0972a5b4..a50323cf 100644 --- a/exercises/17_tests/tests4.rs +++ b/exercises/17_tests/tests4.rs @@ -7,7 +7,7 @@ struct Rectangle { width: i32, - height: i32 + height: i32, } impl Rectangle { @@ -16,10 +16,14 @@ impl Rectangle { if width <= 0 || height <= 0 { panic!("Rectangle width and height cannot be negative!") } - Rectangle {width, height} + Rectangle { width, height } } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index 4ca7742e..0ebd69a1 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -33,6 +33,10 @@ pub fn capitalize_words_string(words: &[&str]) -> String { String::new() } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index f7da049c..3f5923cd 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -43,6 +43,10 @@ fn list_of_results() -> () { let division_results = numbers.into_iter().map(|n| divide(n, 27)); } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index af3958c8..8fc87927 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -15,6 +15,10 @@ pub fn factorial(num: u64) -> u64 { // Execute `rustlings hint iterators4` for hints. } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs index ceec5369..2604004a 100644 --- a/exercises/18_iterators/iterators5.rs +++ b/exercises/18_iterators/iterators5.rs @@ -55,6 +55,10 @@ fn count_collection_iterator(collection: &[HashMap], value: Pr todo!(); } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs index b24591b7..51e5fdb4 100644 --- a/exercises/19_smart_pointers/cow1.rs +++ b/exercises/19_smart_pointers/cow1.rs @@ -25,6 +25,10 @@ fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> { input } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index cd2c93be..6fb7c2fc 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -26,6 +26,10 @@ fn num_sq(arg: &mut T) { ??? } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs index b9e71f59..55bc61f6 100644 --- a/exercises/quiz1.rs +++ b/exercises/quiz1.rs @@ -16,6 +16,10 @@ // Put your function here! // fn calculate_price_of_apples { +fn main() { + // You can optionally experiment here. +} + // Don't modify this function! #[test] fn verify_test() { diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs index 8ace3fe0..1d73ab93 100644 --- a/exercises/quiz2.rs +++ b/exercises/quiz2.rs @@ -40,6 +40,10 @@ mod my_module { } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { // TODO: What do we need to import to have `transformer` in scope? diff --git a/exercises/quiz3.rs b/exercises/quiz3.rs index 24f70829..780e130d 100644 --- a/exercises/quiz3.rs +++ b/exercises/quiz3.rs @@ -24,11 +24,17 @@ pub struct ReportCard { impl ReportCard { pub fn print(&self) -> String { - format!("{} ({}) - achieved a grade of {}", - &self.student_name, &self.student_age, &self.grade) + format!( + "{} ({}) - achieved a grade of {}", + &self.student_name, &self.student_age, &self.grade + ) } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dev/check.rs b/src/dev/check.rs index 4688e044..d2e5fe14 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,8 +1,9 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, - fs::{self, read_dir}, - path::PathBuf, + fs::{self, read_dir, OpenOptions}, + io::Read, + path::{Path, PathBuf}, }; use crate::{ @@ -18,6 +19,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result Result Result, -) -> Result> { - let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); +fn unexpected_file(path: &Path) -> Error { + anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display()) +} +fn check_exercise_dir_files(info_file_paths: hashbrown::HashSet) -> Result<()> { for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { let entry = entry.context("Failed to read the `exercises` directory")?; @@ -72,11 +91,9 @@ fn check_exercise_dir_files( } if !info_file_paths.contains(&path) { - bail!("`{}` is expected to be an exercise file corresponding to some exercise in `info.toml`", path.display()); + return Err(unexpected_file(&path)); } - let file_name = file_name.to_string_lossy(); - names.insert(file_name[..file_name.len() - 3].to_string()); continue; } @@ -89,7 +106,7 @@ fn check_exercise_dir_files( let path = entry.path(); if !entry.file_type().unwrap().is_file() { - bail!("Found {} but expected only files", path.display()); + bail!("Found `{}` but expected only files. Only one level of exercise nesting is allowed", path.display()); } let file_name = path.file_name().unwrap(); @@ -98,21 +115,15 @@ fn check_exercise_dir_files( } if !info_file_paths.contains(&path) { - bail!("`{}` is expected to be an exercise file corresponding to some exercise in `info.toml`", path.display()); + return Err(unexpected_file(&path)); } - - // The file name must be valid Unicode with the `.rs` extension - // because it is part of the info file paths. - let file_name = file_name.to_string_lossy(); - let file_name_without_rs_extension = file_name[..file_name.len() - 3].to_string(); - names.insert(file_name_without_rs_extension); } } - Ok(names) + Ok(()) } -fn check_info_file(info_file: &InfoFile) -> Result<()> { +fn check_exercises(info_file: &InfoFile) -> Result<()> { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), @@ -120,17 +131,7 @@ fn check_info_file(info_file: &InfoFile) -> Result<()> { } let info_file_paths = check_info_file_exercises(info_file)?; - let names_in_exercises_dir = check_exercise_dir_files(info_file, info_file_paths)?; - - // Now, we know that every file has an exercise in `info.toml`. - // But we need to check that every exercise in `info.toml` has a file. - if names_in_exercises_dir.len() != info_file.exercises.len() { - for exercise_info in &info_file.exercises { - if !names_in_exercises_dir.contains(&exercise_info.name) { - bail!("The file `{}` is missing", exercise_info.path()); - } - } - } + check_exercise_dir_files(info_file_paths)?; Ok(()) } @@ -190,7 +191,7 @@ fn check_cargo_toml( pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; - check_info_file(&info_file)?; + check_exercises(&info_file)?; if DEVELOPING_OFFICIAL_RUSTLINGS { check_cargo_toml( From 2f810a4da67233716ad93e00afff6e8b260f4807 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 23:34:27 +0200 Subject: [PATCH 173/433] Clean up and unify exercises --- exercises/00_intro/intro1.rs | 5 - exercises/00_intro/intro2.rs | 5 - exercises/01_variables/variables1.rs | 5 - exercises/01_variables/variables2.rs | 5 - exercises/01_variables/variables3.rs | 5 - exercises/01_variables/variables4.rs | 5 - exercises/01_variables/variables5.rs | 5 - exercises/01_variables/variables6.rs | 5 - exercises/02_functions/functions1.rs | 5 - exercises/02_functions/functions2.rs | 5 - exercises/02_functions/functions3.rs | 5 - exercises/02_functions/functions4.rs | 5 - exercises/02_functions/functions5.rs | 5 - exercises/03_if/if1.rs | 4 - exercises/03_if/if2.rs | 4 - exercises/03_if/if3.rs | 4 - .../04_primitive_types/primitive_types1.rs | 5 +- .../04_primitive_types/primitive_types2.rs | 5 - .../04_primitive_types/primitive_types3.rs | 5 - .../04_primitive_types/primitive_types4.rs | 20 +-- .../04_primitive_types/primitive_types5.rs | 5 - .../04_primitive_types/primitive_types6.rs | 24 +-- exercises/05_vecs/vecs1.rs | 4 - exercises/05_vecs/vecs2.rs | 4 - .../06_move_semantics/move_semantics1.rs | 32 ++-- .../06_move_semantics/move_semantics2.rs | 34 ++-- .../06_move_semantics/move_semantics3.rs | 32 ++-- .../06_move_semantics/move_semantics4.rs | 34 ++-- .../06_move_semantics/move_semantics5.rs | 30 ++-- .../06_move_semantics/move_semantics6.rs | 5 - exercises/07_structs/structs1.rs | 5 - exercises/07_structs/structs2.rs | 5 - exercises/07_structs/structs3.rs | 5 - exercises/08_enums/enums1.rs | 4 - exercises/08_enums/enums2.rs | 5 - exercises/08_enums/enums3.rs | 5 - exercises/09_strings/strings1.rs | 5 - exercises/09_strings/strings2.rs | 5 - exercises/09_strings/strings3.rs | 5 - exercises/09_strings/strings4.rs | 6 +- exercises/10_modules/modules1.rs | 5 - exercises/10_modules/modules2.rs | 5 - exercises/10_modules/modules3.rs | 5 - exercises/11_hashmaps/hashmaps1.rs | 5 - exercises/11_hashmaps/hashmaps2.rs | 5 - exercises/11_hashmaps/hashmaps3.rs | 5 - exercises/12_options/options1.rs | 5 - exercises/12_options/options2.rs | 5 - exercises/12_options/options3.rs | 5 - exercises/13_error_handling/errors1.rs | 5 - exercises/13_error_handling/errors2.rs | 5 - exercises/13_error_handling/errors3.rs | 5 - exercises/13_error_handling/errors4.rs | 26 ++-- exercises/13_error_handling/errors5.rs | 5 - exercises/13_error_handling/errors6.rs | 5 - exercises/14_generics/generics1.rs | 5 - exercises/14_generics/generics2.rs | 5 - exercises/15_traits/traits1.rs | 5 - exercises/15_traits/traits2.rs | 4 - exercises/15_traits/traits3.rs | 5 - exercises/15_traits/traits4.rs | 5 - exercises/15_traits/traits5.rs | 5 - exercises/16_lifetimes/lifetimes1.rs | 5 - exercises/16_lifetimes/lifetimes2.rs | 5 - exercises/16_lifetimes/lifetimes3.rs | 10 +- exercises/17_tests/tests1.rs | 5 - exercises/17_tests/tests2.rs | 5 - exercises/17_tests/tests3.rs | 5 - exercises/17_tests/tests4.rs | 5 - exercises/18_iterators/iterators1.rs | 36 +++-- exercises/18_iterators/iterators2.rs | 5 - exercises/18_iterators/iterators3.rs | 5 - exercises/18_iterators/iterators4.rs | 6 - exercises/18_iterators/iterators5.rs | 5 - exercises/19_smart_pointers/arc1.rs | 4 - exercises/19_smart_pointers/box1.rs | 4 - exercises/19_smart_pointers/cow1.rs | 4 - exercises/19_smart_pointers/rc1.rs | 145 +++++++++--------- exercises/20_threads/threads1.rs | 5 - exercises/20_threads/threads2.rs | 5 - exercises/20_threads/threads3.rs | 44 +++--- exercises/21_macros/macros1.rs | 5 - exercises/21_macros/macros2.rs | 5 - exercises/21_macros/macros3.rs | 5 - exercises/21_macros/macros4.rs | 5 - exercises/22_clippy/clippy1.rs | 5 - exercises/22_clippy/clippy2.rs | 5 - exercises/22_clippy/clippy3.rs | 3 - exercises/23_conversions/as_ref_mut.rs | 5 - exercises/23_conversions/from_into.rs | 6 - exercises/23_conversions/from_str.rs | 5 - exercises/23_conversions/try_from_into.rs | 5 - exercises/23_conversions/using_as.rs | 5 - exercises/quiz1.rs | 31 ++-- exercises/quiz2.rs | 4 - exercises/quiz3.rs | 4 - 96 files changed, 267 insertions(+), 635 deletions(-) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 170d1958..70000392 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,5 +1,3 @@ -// intro1.rs -// // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel // ready for the next exercise, remove the `I AM NOT DONE` comment below. @@ -8,9 +6,6 @@ // reloaded when you change one of the lines below! Try adding a `println!` // line, or try changing what it outputs in your terminal. Try removing a // semicolon and see what happens! -// -// Execute `rustlings hint intro1` or use the `hint` watch subcommand for a -// hint. fn main() { println!("Hello and"); diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs index 84e0d75c..c7a3ab2a 100644 --- a/exercises/00_intro/intro2.rs +++ b/exercises/00_intro/intro2.rs @@ -1,9 +1,4 @@ -// intro2.rs -// // Make the code print a greeting to the world. -// -// Execute `rustlings hint intro2` or use the `hint` watch subcommand for a -// hint. fn main() { printline!("Hello there!") diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs index 56408f35..3035bfae 100644 --- a/exercises/01_variables/variables1.rs +++ b/exercises/01_variables/variables1.rs @@ -1,9 +1,4 @@ -// variables1.rs -// // Make me compile! -// -// Execute `rustlings hint variables1` or use the `hint` watch subcommand for a -// hint. fn main() { x = 5; diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs index 0f417e01..ce2dd851 100644 --- a/exercises/01_variables/variables2.rs +++ b/exercises/01_variables/variables2.rs @@ -1,8 +1,3 @@ -// variables2.rs -// -// Execute `rustlings hint variables2` or use the `hint` watch subcommand for a -// hint. - fn main() { let x; if x == 10 { diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs index 421c6b15..488385ba 100644 --- a/exercises/01_variables/variables3.rs +++ b/exercises/01_variables/variables3.rs @@ -1,8 +1,3 @@ -// variables3.rs -// -// Execute `rustlings hint variables3` or use the `hint` watch subcommand for a -// hint. - fn main() { let x: i32; println!("Number {}", x); diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs index 68f8f50b..67be1271 100644 --- a/exercises/01_variables/variables4.rs +++ b/exercises/01_variables/variables4.rs @@ -1,8 +1,3 @@ -// variables4.rs -// -// Execute `rustlings hint variables4` or use the `hint` watch subcommand for a -// hint. - fn main() { let x = 3; println!("Number {}", x); diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index 7014c568..3a745411 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -1,8 +1,3 @@ -// variables5.rs -// -// Execute `rustlings hint variables5` or use the `hint` watch subcommand for a -// hint. - fn main() { let number = "T-H-R-E-E"; // don't change this line println!("Spell a Number : {}", number); diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs index 9f476825..4746331b 100644 --- a/exercises/01_variables/variables6.rs +++ b/exercises/01_variables/variables6.rs @@ -1,8 +1,3 @@ -// variables6.rs -// -// Execute `rustlings hint variables6` or use the `hint` watch subcommand for a -// hint. - const NUMBER = 3; fn main() { println!("Number {}", NUMBER); diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs index 2365f91b..4e3b1036 100644 --- a/exercises/02_functions/functions1.rs +++ b/exercises/02_functions/functions1.rs @@ -1,8 +1,3 @@ -// functions1.rs -// -// Execute `rustlings hint functions1` or use the `hint` watch subcommand for a -// hint. - fn main() { call_me(); } diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs index 64dbd665..84e09cda 100644 --- a/exercises/02_functions/functions2.rs +++ b/exercises/02_functions/functions2.rs @@ -1,8 +1,3 @@ -// functions2.rs -// -// Execute `rustlings hint functions2` or use the `hint` watch subcommand for a -// hint. - fn main() { call_me(3); } diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs index 50371212..66fb6d32 100644 --- a/exercises/02_functions/functions3.rs +++ b/exercises/02_functions/functions3.rs @@ -1,8 +1,3 @@ -// functions3.rs -// -// Execute `rustlings hint functions3` or use the `hint` watch subcommand for a -// hint. - fn main() { call_me(); } diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs index 6b449edf..06d3b1bb 100644 --- a/exercises/02_functions/functions4.rs +++ b/exercises/02_functions/functions4.rs @@ -1,12 +1,7 @@ -// functions4.rs -// // This store is having a sale where if the price is an even number, you get 10 // Rustbucks off, but if it's an odd number, it's 3 Rustbucks off. (Don't worry // about the function bodies themselves, we're only interested in the signatures // for now. If anything, this is a good way to peek ahead to future exercises!) -// -// Execute `rustlings hint functions4` or use the `hint` watch subcommand for a -// hint. fn main() { let original_price = 51; diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs index 0c963223..3bb5e52a 100644 --- a/exercises/02_functions/functions5.rs +++ b/exercises/02_functions/functions5.rs @@ -1,8 +1,3 @@ -// functions5.rs -// -// Execute `rustlings hint functions5` or use the `hint` watch subcommand for a -// hint. - fn main() { let answer = square(3); println!("The square of 3 is {}", answer); diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs index dbd0d285..52dee0b5 100644 --- a/exercises/03_if/if1.rs +++ b/exercises/03_if/if1.rs @@ -1,7 +1,3 @@ -// if1.rs -// -// Execute `rustlings hint if1` or use the `hint` watch subcommand for a hint. - pub fn bigger(a: i32, b: i32) -> i32 { // Complete this function to return the bigger number! // If both numbers are equal, any of them can be returned. diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index a1ed5c8b..a06bba55 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -1,9 +1,5 @@ -// if2.rs -// // Step 1: Make me compile! // Step 2: Get the bar_for_fuzz and default_to_baz tests passing! -// -// Execute `rustlings hint if2` or use the `hint` watch subcommand for a hint. pub fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index 0b44c5ab..1d9b7c25 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -1,7 +1,3 @@ -// if3.rs -// -// Execute `rustlings hint if3` or use the `hint` watch subcommand for a hint. - pub fn animal_habitat(animal: &str) -> &'static str { let identifier = if animal == "crab" { 1 diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs index f9169c84..0002651d 100644 --- a/exercises/04_primitive_types/primitive_types1.rs +++ b/exercises/04_primitive_types/primitive_types1.rs @@ -1,7 +1,4 @@ -// primitive_types1.rs -// -// Fill in the rest of the line that has code missing! No hints, there's no -// tricks, just get used to typing these :) +// Fill in the rest of the line that has code missing! fn main() { // Booleans (`bool`) diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs index 1911b12a..29c74718 100644 --- a/exercises/04_primitive_types/primitive_types2.rs +++ b/exercises/04_primitive_types/primitive_types2.rs @@ -1,8 +1,3 @@ -// primitive_types2.rs -// -// Fill in the rest of the line that has code missing! No hints, there's no -// tricks, just get used to typing these :) - fn main() { // Characters (`char`) diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs index 70a8cc20..5095fc4a 100644 --- a/exercises/04_primitive_types/primitive_types3.rs +++ b/exercises/04_primitive_types/primitive_types3.rs @@ -1,9 +1,4 @@ -// primitive_types3.rs -// // Create an array with at least 100 elements in it where the ??? is. -// -// Execute `rustlings hint primitive_types3` or use the `hint` watch subcommand -// for a hint. fn main() { let a = ??? diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index f99d8895..c583ae13 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -1,19 +1,19 @@ -// primitive_types4.rs -// // Get a slice out of Array a where the ??? is so that the test passes. -// -// Execute `rustlings hint primitive_types4` or use the `hint` watch subcommand -// for a hint. fn main() { // You can optionally experiment here. } -#[test] -fn slice_out_of_array() { - let a = [1, 2, 3, 4, 5]; +#[cfg(test)] +mod tests { + use super::*; - let nice_slice = ??? + #[test] + fn slice_out_of_array() { + let a = [1, 2, 3, 4, 5]; - assert_eq!([2, 3, 4], nice_slice) + let nice_slice = ??? + + assert_eq!([2, 3, 4], nice_slice) + } } diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs index 5754a3d8..f2216a55 100644 --- a/exercises/04_primitive_types/primitive_types5.rs +++ b/exercises/04_primitive_types/primitive_types5.rs @@ -1,9 +1,4 @@ -// primitive_types5.rs -// // Destructure the `cat` tuple so that the println will work. -// -// Execute `rustlings hint primitive_types5` or use the `hint` watch subcommand -// for a hint. fn main() { let cat = ("Furry McFurson", 3.5); diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs index 48e84d34..83cec24b 100644 --- a/exercises/04_primitive_types/primitive_types6.rs +++ b/exercises/04_primitive_types/primitive_types6.rs @@ -1,21 +1,21 @@ -// primitive_types6.rs -// // Use a tuple index to access the second element of `numbers`. You can put the // expression for the second element where ??? is so that the test passes. -// -// Execute `rustlings hint primitive_types6` or use the `hint` watch subcommand -// for a hint. fn main() { // You can optionally experiment here. } -#[test] -fn indexing_tuple() { - let numbers = (1, 2, 3); - // Replace below ??? with the tuple indexing syntax. - let second = ???; +#[cfg(test)] +mod tests { + use super::*; - assert_eq!(2, second, - "This is not the 2nd number in the tuple!") + #[test] + fn indexing_tuple() { + let numbers = (1, 2, 3); + // Replace below ??? with the tuple indexing syntax. + let second = ???; + + assert_eq!(2, second, + "This is not the 2nd number in the tuple!") + } } diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index 5f44cb24..ddcad84b 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -1,11 +1,7 @@ -// vecs1.rs -// // Your task is to create a `Vec` which holds the exact same elements as in the // array `a`. // // Make me compile and pass the test! -// -// Execute `rustlings hint vecs1` or use the `hint` watch subcommand for a hint. fn array_and_vec() -> ([i32; 4], Vec) { let a = [10, 20, 30, 40]; // a plain array diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index 1b16f0b4..e72209c4 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -1,11 +1,7 @@ -// vecs2.rs -// // A Vec of even numbers is given. Your task is to complete the loop so that // each number in the Vec is multiplied by 2. // // Make me pass the test! -// -// Execute `rustlings hint vecs2` or use the `hint` watch subcommand for a hint. fn vec_loop(mut v: Vec) -> Vec { for element in v.iter_mut() { diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs index c612ba93..8c3fe3a7 100644 --- a/exercises/06_move_semantics/move_semantics1.rs +++ b/exercises/06_move_semantics/move_semantics1.rs @@ -1,17 +1,3 @@ -// move_semantics1.rs -// -// Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand -// for a hint. - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); -} - fn fill_vec(vec: Vec) -> Vec { let vec = vec; @@ -19,3 +5,21 @@ fn fill_vec(vec: Vec) -> Vec { vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics1() { + let vec0 = vec![22, 44, 66]; + + let vec1 = fill_vec(vec0); + + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs index 3457d111..d0879113 100644 --- a/exercises/06_move_semantics/move_semantics2.rs +++ b/exercises/06_move_semantics/move_semantics2.rs @@ -1,19 +1,4 @@ -// move_semantics2.rs -// // Make the test pass by finding a way to keep both Vecs separate! -// -// Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand -// for a hint. - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec0, vec![22, 44, 66]); - assert_eq!(vec1, vec![22, 44, 66, 88]); -} fn fill_vec(vec: Vec) -> Vec { let mut vec = vec; @@ -22,3 +7,22 @@ fn fill_vec(vec: Vec) -> Vec { vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics2() { + let vec0 = vec![22, 44, 66]; + + let vec1 = fill_vec(vec0); + + assert_eq!(vec0, vec![22, 44, 66]); + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs index 9415eb15..24e35971 100644 --- a/exercises/06_move_semantics/move_semantics3.rs +++ b/exercises/06_move_semantics/move_semantics3.rs @@ -1,22 +1,26 @@ -// move_semantics3.rs -// // Make me compile without adding new lines -- just changing existing lines! (no // lines with multiple semicolons necessary!) -// -// Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand -// for a hint. - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); -} fn fill_vec(vec: Vec) -> Vec { vec.push(88); vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics3() { + let vec0 = vec![22, 44, 66]; + + let vec1 = fill_vec(vec0); + + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs index 1509f5d2..b6622244 100644 --- a/exercises/06_move_semantics/move_semantics4.rs +++ b/exercises/06_move_semantics/move_semantics4.rs @@ -1,20 +1,6 @@ -// move_semantics4.rs -// // Refactor this code so that instead of passing `vec0` into the `fill_vec` // function, the Vector gets created in the function itself and passed back to -// the main function. -// -// Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand -// for a hint. - -#[test] -fn main() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); -} +// the test function. // `fill_vec()` no longer takes `vec: Vec` as argument - don't change this! fn fill_vec() -> Vec { @@ -25,3 +11,21 @@ fn fill_vec() -> Vec { vec } + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics4() { + let vec0 = vec![22, 44, 66]; + + let vec1 = fill_vec(vec0); + + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs index c84d2fea..b34560ab 100644 --- a/exercises/06_move_semantics/move_semantics5.rs +++ b/exercises/06_move_semantics/move_semantics5.rs @@ -1,17 +1,21 @@ -// move_semantics5.rs -// -// Make me compile only by reordering the lines in `main()`, but without adding, +// Make me compile only by reordering the lines in the test, but without adding, // changing or removing any of them. -// -// Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand -// for a hint. -#[test] fn main() { - let mut x = 100; - let y = &mut x; - let z = &mut x; - *y += 100; - *z += 1000; - assert_eq!(x, 1200); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics5() { + let mut x = 100; + let y = &mut x; + let z = &mut x; + *y += 100; + *z += 1000; + assert_eq!(x, 1200); + } } diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs index 6059e61c..2ad71db2 100644 --- a/exercises/06_move_semantics/move_semantics6.rs +++ b/exercises/06_move_semantics/move_semantics6.rs @@ -1,9 +1,4 @@ -// move_semantics6.rs -// // You can't change anything except adding or removing references. -// -// Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand -// for a hint. fn main() { let data = "Rust is great!".to_string(); diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs index cd8b81c9..62f14219 100644 --- a/exercises/07_structs/structs1.rs +++ b/exercises/07_structs/structs1.rs @@ -1,9 +1,4 @@ -// structs1.rs -// // Address all the TODOs to make the tests pass! -// -// Execute `rustlings hint structs1` or use the `hint` watch subcommand for a -// hint. struct ColorClassicStruct { // TODO: Something goes here diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs index 7e61e752..451dbe76 100644 --- a/exercises/07_structs/structs2.rs +++ b/exercises/07_structs/structs2.rs @@ -1,9 +1,4 @@ -// structs2.rs -// // Address all the TODOs to make the tests pass! -// -// Execute `rustlings hint structs2` or use the `hint` watch subcommand for a -// hint. #[derive(Debug)] struct Order { diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index bd562a12..10adb487 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -1,11 +1,6 @@ -// structs3.rs -// // Structs contain data, but can also have logic. In this exercise we have // defined the Package struct and we want to test some logic attached to it. // Make the code compile and the tests pass! -// -// Execute `rustlings hint structs3` or use the `hint` watch subcommand for a -// hint. #[derive(Debug)] struct Package { diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs index 330269ca..d63de83f 100644 --- a/exercises/08_enums/enums1.rs +++ b/exercises/08_enums/enums1.rs @@ -1,7 +1,3 @@ -// enums1.rs -// -// No hints this time! ;) - #[derive(Debug)] enum Message { // TODO: define a few types of messages as used below diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs index f0e4e6d3..f3b803ff 100644 --- a/exercises/08_enums/enums2.rs +++ b/exercises/08_enums/enums2.rs @@ -1,8 +1,3 @@ -// enums2.rs -// -// Execute `rustlings hint enums2` or use the `hint` watch subcommand for a -// hint. - #[derive(Debug)] enum Message { // TODO: define the different variants used below diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index 56c04fe6..edac3dfb 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -1,9 +1,4 @@ -// enums3.rs -// // Address all the TODOs to make the tests pass! -// -// Execute `rustlings hint enums3` or use the `hint` watch subcommand for a -// hint. enum Message { // TODO: implement the message variant types based on their usage below diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs index a1255a32..de762ebf 100644 --- a/exercises/09_strings/strings1.rs +++ b/exercises/09_strings/strings1.rs @@ -1,9 +1,4 @@ -// strings1.rs -// // Make me compile without changing the function signature! -// -// Execute `rustlings hint strings1` or use the `hint` watch subcommand for a -// hint. fn main() { let answer = current_favorite_color(); diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs index ba76fe65..47682781 100644 --- a/exercises/09_strings/strings2.rs +++ b/exercises/09_strings/strings2.rs @@ -1,9 +1,4 @@ -// strings2.rs -// // Make me compile without changing the function signature! -// -// Execute `rustlings hint strings2` or use the `hint` watch subcommand for a -// hint. fn main() { let word = String::from("green"); // Try not changing this line :) diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index d53f654e..f83a5310 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -1,8 +1,3 @@ -// strings3.rs -// -// Execute `rustlings hint strings3` or use the `hint` watch subcommand for a -// hint. - fn trim_me(input: &str) -> String { // TODO: Remove whitespace from both ends of a string! ??? diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index a034aa49..1f3d88b7 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -1,11 +1,7 @@ -// strings4.rs -// -// Ok, here are a bunch of values-- some are `String`s, some are `&str`s. Your +// Ok, here are a bunch of values - some are `String`s, some are `&str`s. Your // task is to call one of these two functions on each value depending on what // you think each value is. That is, add either `string_slice` or `string` // before the parentheses on each line. If you're right, it will compile! -// -// No hints this time! fn string_slice(arg: &str) { println!("{}", arg); diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs index c750946c..931a3e26 100644 --- a/exercises/10_modules/modules1.rs +++ b/exercises/10_modules/modules1.rs @@ -1,8 +1,3 @@ -// modules1.rs -// -// Execute `rustlings hint modules1` or use the `hint` watch subcommand for a -// hint. - mod sausage_factory { // Don't let anybody outside of this module see this! fn get_secret_recipe() -> String { diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 4d3106c4..5f8b0d51 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -1,11 +1,6 @@ -// modules2.rs -// // You can bring module paths into scopes and provide new names for them with // the 'use' and 'as' keywords. Fix these 'use' statements to make the code // compile. -// -// Execute `rustlings hint modules2` or use the `hint` watch subcommand for a -// hint. mod delicious_snacks { // TODO: Fix these use statements diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs index c211a769..eff24a9c 100644 --- a/exercises/10_modules/modules3.rs +++ b/exercises/10_modules/modules3.rs @@ -1,12 +1,7 @@ -// modules3.rs -// // You can use the 'use' keyword to bring module paths from modules from // anywhere and especially from the Rust standard library into your scope. Bring // SystemTime and UNIX_EPOCH from the std::time module. Bonus style points if // you can do it with one line! -// -// Execute `rustlings hint modules3` or use the `hint` watch subcommand for a -// hint. // TODO: Complete this use statement use ??? diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs index 51146dfc..e646ed71 100644 --- a/exercises/11_hashmaps/hashmaps1.rs +++ b/exercises/11_hashmaps/hashmaps1.rs @@ -1,5 +1,3 @@ -// hashmaps1.rs -// // A basket of fruits in the form of a hash map needs to be defined. The key // represents the name of the fruit and the value represents how many of that // particular fruit is in the basket. You have to put at least three different @@ -7,9 +5,6 @@ // of all the fruits should be at least five. // // Make me compile and pass the tests! -// -// Execute `rustlings hint hashmaps1` or use the `hint` watch subcommand for a -// hint. use std::collections::HashMap; diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index 47983f6b..e6380d94 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -1,5 +1,3 @@ -// hashmaps2.rs -// // We're collecting different fruits to bake a delicious fruit cake. For this, // we have a basket, which we'll represent in the form of a hash map. The key // represents the name of each fruit we collect and the value represents how @@ -10,9 +8,6 @@ // to insert any more of these fruits! // // Make me pass the tests! -// -// Execute `rustlings hint hashmaps2` or use the `hint` watch subcommand for a -// hint. use std::collections::HashMap; diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 33229096..070c3709 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -1,5 +1,3 @@ -// hashmaps3.rs -// // A list of scores (one per line) of a soccer match is given. Each line is of // the form : ",,," // Example: England,France,4,2 (England scored 4 goals, France 2). @@ -11,9 +9,6 @@ // complete it to pass the test. // // Make me pass the tests! -// -// Execute `rustlings hint hashmaps3` or use the `hint` watch subcommand for a -// hint. use std::collections::HashMap; diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index aecb123a..b7cf7b0b 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -1,8 +1,3 @@ -// options1.rs -// -// Execute `rustlings hint options1` or use the `hint` watch subcommand for a -// hint. - // This function returns how much icecream there is left in the fridge. // If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it // all, so there'll be no more left :( diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs index d183d1d5..01f84c58 100644 --- a/exercises/12_options/options2.rs +++ b/exercises/12_options/options2.rs @@ -1,8 +1,3 @@ -// options2.rs -// -// Execute `rustlings hint options2` or use the `hint` watch subcommand for a -// hint. - fn main() { // You can optionally experiment here. } diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs index 7922ef92..5b70a792 100644 --- a/exercises/12_options/options3.rs +++ b/exercises/12_options/options3.rs @@ -1,8 +1,3 @@ -// options3.rs -// -// Execute `rustlings hint options3` or use the `hint` watch subcommand for a -// hint. - struct Point { x: i32, y: i32, diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 7991c420..15a3716d 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -1,13 +1,8 @@ -// errors1.rs -// // This function refuses to generate text to be printed on a nametag if you pass // it an empty string. It'd be nicer if it explained what the problem was, // instead of just sometimes returning `None`. Thankfully, Rust has a similar // construct to `Option` that can be used to express error conditions. Let's use // it! -// -// Execute `rustlings hint errors1` or use the `hint` watch subcommand for a -// hint. fn main() { // You can optionally experiment here. diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index 051516b9..e39aa959 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -1,5 +1,3 @@ -// errors2.rs -// // Say we're writing a game where you can buy items with tokens. All items cost // 5 tokens, and whenever you purchase items there is a processing fee of 1 // token. A player of the game will type in how many items they want to buy, and @@ -15,9 +13,6 @@ // // There are at least two ways to implement this that are both correct-- but one // is a lot shorter! -// -// Execute `rustlings hint errors2` or use the `hint` watch subcommand for a -// hint. use std::num::ParseIntError; diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index 56bb31b1..5661f17b 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -1,11 +1,6 @@ -// errors3.rs -// // This is a program that is trying to use a completed version of the // `total_cost` function from the previous exercise. It's not working though! // Why not? What should we do to fix it? -// -// Execute `rustlings hint errors3` or use the `hint` watch subcommand for a -// hint. use std::num::ParseIntError; diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index 94494170..993d42a1 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -1,8 +1,3 @@ -// errors4.rs -// -// Execute `rustlings hint errors4` or use the `hint` watch subcommand for a -// hint. - #[derive(PartialEq, Debug)] struct PositiveNonzeroInteger(u64); @@ -23,12 +18,17 @@ fn main() { // You can optionally experiment here. } -#[test] -fn test_creation() { - assert!(PositiveNonzeroInteger::new(10).is_ok()); - assert_eq!( - Err(CreationError::Negative), - PositiveNonzeroInteger::new(-10) - ); - assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0)); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_creation() { + assert!(PositiveNonzeroInteger::new(10).is_ok()); + assert_eq!( + Err(CreationError::Negative), + PositiveNonzeroInteger::new(-10) + ); + assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0)); + } } diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 0bcb4b8c..71925626 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -1,5 +1,3 @@ -// errors5.rs -// // This program uses an altered version of the code from errors4. // // This exercise uses some concepts that we won't get to until later in the @@ -18,9 +16,6 @@ // // What can we use to describe both errors? In other words, is there a trait // which both errors implement? -// -// Execute `rustlings hint errors5` or use the `hint` watch subcommand for a -// hint. use std::error; use std::fmt; diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index 363a3b91..8b08e086 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -1,13 +1,8 @@ -// errors6.rs -// // Using catch-all error types like `Box` isn't recommended // for library code, where callers might want to make decisions based on the // error content, instead of printing it out or propagating it further. Here, we // define a custom error type to make it possible for callers to decide what to // do next when our function returns an error. -// -// Execute `rustlings hint errors6` or use the `hint` watch subcommand for a -// hint. use std::num::ParseIntError; diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs index 545fd95c..c023e644 100644 --- a/exercises/14_generics/generics1.rs +++ b/exercises/14_generics/generics1.rs @@ -1,10 +1,5 @@ -// generics1.rs -// // This shopping list program isn't compiling! Use your knowledge of generics to // fix it. -// -// Execute `rustlings hint generics1` or use the `hint` watch subcommand for a -// hint. fn main() { let mut shopping_list: Vec = Vec::new(); diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index 068468ba..cbb9b5f9 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -1,10 +1,5 @@ -// generics2.rs -// // This powerful wrapper provides the ability to store a positive integer value. // Rewrite it using generics so that it supports wrapping ANY type. -// -// Execute `rustlings hint generics2` or use the `hint` watch subcommand for a -// hint. struct Wrapper { value: u32, diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs index c51d3b88..b17c9c6e 100644 --- a/exercises/15_traits/traits1.rs +++ b/exercises/15_traits/traits1.rs @@ -1,11 +1,6 @@ -// traits1.rs -// // Time to implement some traits! Your task is to implement the trait // `AppendBar` for the type `String`. The trait AppendBar has only one function, // which appends "Bar" to any object implementing this trait. -// -// Execute `rustlings hint traits1` or use the `hint` watch subcommand for a -// hint. trait AppendBar { fn append_bar(self) -> Self; diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index 18ebcb08..170779b2 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -1,12 +1,8 @@ -// traits2.rs -// // Your task is to implement the trait `AppendBar` for a vector of strings. To // implement this trait, consider for a moment what it means to 'append "Bar"' // to a vector of strings. // // No boiler plate code this time, you can do this! -// -// Execute `rustlings hint traits2` or use the `hint` watch subcommand for a hint. trait AppendBar { fn append_bar(self) -> Self; diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 8412afa2..9a2365ae 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -1,12 +1,7 @@ -// traits3.rs -// // Your task is to implement the Licensed trait for both structures and have // them return the same information without writing the same function twice. // // Consider what you can add to the Licensed trait. -// -// Execute `rustlings hint traits3` or use the `hint` watch subcommand for a -// hint. pub trait Licensed { fn licensing_info(&self) -> String; diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index 18db0d67..7af30b57 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -1,11 +1,6 @@ -// traits4.rs -// // Your task is to replace the '??' sections so the code compiles. // // Don't change any line other than the marked one. -// -// Execute `rustlings hint traits4` or use the `hint` watch subcommand for a -// hint. pub trait Licensed { fn licensing_info(&self) -> String { diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs index f258d327..9a45bb76 100644 --- a/exercises/15_traits/traits5.rs +++ b/exercises/15_traits/traits5.rs @@ -1,11 +1,6 @@ -// traits5.rs -// // Your task is to replace the '??' sections so the code compiles. // // Don't change any line other than the marked one. -// -// Execute `rustlings hint traits5` or use the `hint` watch subcommand for a -// hint. pub trait SomeTrait { fn some_function(&self) -> bool { diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs index 4f544b41..d34f3abd 100644 --- a/exercises/16_lifetimes/lifetimes1.rs +++ b/exercises/16_lifetimes/lifetimes1.rs @@ -1,12 +1,7 @@ -// lifetimes1.rs -// // The Rust compiler needs to know how to check whether supplied references are // valid, so that it can let the programmer know if a reference is at risk of // going out of scope before it is used. Remember, references are borrows and do // not own their own data. What if their owner goes out of scope? -// -// Execute `rustlings hint lifetimes1` or use the `hint` watch subcommand for a -// hint. fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs index 33b5565f..6e329e6d 100644 --- a/exercises/16_lifetimes/lifetimes2.rs +++ b/exercises/16_lifetimes/lifetimes2.rs @@ -1,10 +1,5 @@ -// lifetimes2.rs -// // So if the compiler is just validating the references passed to the annotated // parameters and the return type, what do we need to change? -// -// Execute `rustlings hint lifetimes2` or use the `hint` watch subcommand for a -// hint. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs index de6005ec..9b631cab 100644 --- a/exercises/16_lifetimes/lifetimes3.rs +++ b/exercises/16_lifetimes/lifetimes3.rs @@ -1,9 +1,4 @@ -// lifetimes3.rs -// // Lifetimes are also needed when structs hold references. -// -// Execute `rustlings hint lifetimes3` or use the `hint` watch subcommand for a -// hint. struct Book { author: &str, @@ -13,7 +8,10 @@ struct Book { fn main() { let name = String::from("Jill Smith"); let title = String::from("Fish Flying"); - let book = Book { author: &name, title: &title }; + let book = Book { + author: &name, + title: &title, + }; println!("{} by {}", book.title, book.author); } diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs index d32ace1e..854a1358 100644 --- a/exercises/17_tests/tests1.rs +++ b/exercises/17_tests/tests1.rs @@ -1,14 +1,9 @@ -// tests1.rs -// // Tests are important to ensure that your code does what you think it should // do. Tests can be run on this file with the following command: rustlings run // tests1 // // This test has a problem with it -- make the test compile! Make the test pass! // Make the test fail! -// -// Execute `rustlings hint tests1` or use the `hint` watch subcommand for a -// hint. fn main() { // You can optionally experiment here. diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs index 501c44bc..f0899e1b 100644 --- a/exercises/17_tests/tests2.rs +++ b/exercises/17_tests/tests2.rs @@ -1,10 +1,5 @@ -// tests2.rs -// // This test has a problem with it -- make the test compile! Make the test pass! // Make the test fail! -// -// Execute `rustlings hint tests2` or use the `hint` watch subcommand for a -// hint. fn main() { // You can optionally experiment here. diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index a2093cf2..3b4e1990 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -1,11 +1,6 @@ -// tests3.rs -// // This test isn't testing our function -- make it do that in such a way that // the test passes. Then write a second test that tests whether we get the // result we expect to get when we call `is_even(5)`. -// -// Execute `rustlings hint tests3` or use the `hint` watch subcommand for a -// hint. pub fn is_even(num: i32) -> bool { num % 2 == 0 diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs index a50323cf..35a9a3b5 100644 --- a/exercises/17_tests/tests4.rs +++ b/exercises/17_tests/tests4.rs @@ -1,9 +1,4 @@ -// tests4.rs -// // Make sure that we're testing for the correct conditions! -// -// Execute `rustlings hint tests4` or use the `hint` watch subcommand for a -// hint. struct Rectangle { width: i32, diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index 7ec7da2c..52b704d5 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -1,24 +1,28 @@ -// iterators1.rs -// // When performing operations on elements within a collection, iterators are // essential. This module helps you get familiar with the structure of using an // iterator and how to go through elements within an iterable collection. // // Make me compile by filling in the `???`s -// -// Execute `rustlings hint iterators1` or use the `hint` watch subcommand for a -// hint. -#[test] fn main() { - let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"]; - - let mut my_iterable_fav_fruits = ???; // TODO: Step 1 - - assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 2 - assert_eq!(my_iterable_fav_fruits.next(), Some(&"avocado")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 3 - assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 4 + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn iterators() { + let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"]; + + let mut my_iterable_fav_fruits = ???; // TODO: Step 1 + + assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana")); + assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 2 + assert_eq!(my_iterable_fav_fruits.next(), Some(&"avocado")); + assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 3 + assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry")); + assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 4 + } } diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index 0ebd69a1..df1fa838 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -1,10 +1,5 @@ -// iterators2.rs -// // In this exercise, you'll learn some of the unique advantages that iterators // can offer. Follow the steps to complete the exercise. -// -// Execute `rustlings hint iterators2` or use the `hint` watch subcommand for a -// hint. // Step 1. // Complete the `capitalize_first` function. diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 3f5923cd..9f106aa8 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -1,13 +1,8 @@ -// iterators3.rs -// // This is a bigger exercise than most of the others! You can do it! Here is // your mission, should you choose to accept it: // 1. Complete the divide function to get the first four tests to pass. // 2. Get the remaining tests to pass by completing the result_with_list and // list_of_results functions. -// -// Execute `rustlings hint iterators3` or use the `hint` watch subcommand for a -// hint. #[derive(Debug, PartialEq, Eq)] pub enum DivisionError { diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index 8fc87927..60c7b8d1 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -1,8 +1,3 @@ -// iterators4.rs -// -// Execute `rustlings hint iterators4` or use the `hint` watch subcommand for a -// hint. - pub fn factorial(num: u64) -> u64 { // Complete this function to return the factorial of num // Do not use: @@ -12,7 +7,6 @@ pub fn factorial(num: u64) -> u64 { // - additional variables // For an extra challenge, don't use: // - recursion - // Execute `rustlings hint iterators4` for hints. } fn main() { diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs index 2604004a..4f052d51 100644 --- a/exercises/18_iterators/iterators5.rs +++ b/exercises/18_iterators/iterators5.rs @@ -1,5 +1,3 @@ -// iterators5.rs -// // Let's define a simple model to track Rustlings exercise progress. Progress // will be modelled using a hash map. The name of the exercise is the key and // the progress is the value. Two counting functions were created to count the @@ -7,9 +5,6 @@ // functionality using iterators. Try not to use imperative loops (for, while). // Only the two iterator methods (count_iterator and count_collection_iterator) // need to be modified. -// -// Execute `rustlings hint iterators5` or use the `hint` watch subcommand for a -// hint. use std::collections::HashMap; diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs index 0647eea7..7b31fa82 100644 --- a/exercises/19_smart_pointers/arc1.rs +++ b/exercises/19_smart_pointers/arc1.rs @@ -1,5 +1,3 @@ -// arc1.rs -// // In this exercise, we are given a Vec of u32 called "numbers" with values // ranging from 0 to 99 -- [ 0, 1, 2, ..., 98, 99 ] We would like to use this // set of numbers within 8 different threads simultaneously. Each thread is @@ -18,8 +16,6 @@ // first TODO comment is, and create an initial binding for `child_numbers` // where the second TODO comment is. Try not to create any copies of the // `numbers` Vec! -// -// Execute `rustlings hint arc1` or use the `hint` watch subcommand for a hint. #![forbid(unused_imports)] // Do not change this, (or the next) line. use std::sync::Arc; diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index 2abc0249..226a1177 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -1,5 +1,3 @@ -// box1.rs -// // At compile time, Rust needs to know how much space a type takes up. This // becomes problematic for recursive types, where a value can have as part of // itself another value of the same type. To get around the issue, we can use a @@ -15,8 +13,6 @@ // Step 2: create both empty and non-empty cons lists by replacing `todo!()` // // Note: the tests should not be changed -// -// Execute `rustlings hint box1` or use the `hint` watch subcommand for a hint. #[derive(PartialEq, Debug)] pub enum List { diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs index 51e5fdb4..754c0bac 100644 --- a/exercises/19_smart_pointers/cow1.rs +++ b/exercises/19_smart_pointers/cow1.rs @@ -1,5 +1,3 @@ -// cow1.rs -// // This exercise explores the Cow, or Clone-On-Write type. Cow is a // clone-on-write smart pointer. It can enclose and provide immutable access to // borrowed data, and clone the data lazily when mutation or ownership is @@ -9,8 +7,6 @@ // This exercise is meant to show you what to expect when passing data to Cow. // Fix the unit tests by checking for Cow::Owned(_) and Cow::Borrowed(_) at the // TODO markers. -// -// Execute `rustlings hint cow1` or use the `hint` watch subcommand for a hint. use std::borrow::Cow; diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs index e96e6255..19de3db2 100644 --- a/exercises/19_smart_pointers/rc1.rs +++ b/exercises/19_smart_pointers/rc1.rs @@ -1,5 +1,3 @@ -// rc1.rs -// // In this exercise, we want to express the concept of multiple owners via the // Rc type. This is a model of our solar system - there is a Sun type and // multiple Planets. The Planets take ownership of the sun, indicating that they @@ -7,8 +5,6 @@ // // Make this code compile by using the proper Rc primitives to express that the // sun has multiple owners. -// -// Execute `rustlings hint rc1` or use the `hint` watch subcommand for a hint. use std::rc::Rc; @@ -33,71 +29,80 @@ impl Planet { } } -#[test] fn main() { - let sun = Rc::new(Sun {}); - println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference - - let mercury = Planet::Mercury(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 2 references - mercury.details(); - - let venus = Planet::Venus(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 3 references - venus.details(); - - let earth = Planet::Earth(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 4 references - earth.details(); - - let mars = Planet::Mars(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 5 references - mars.details(); - - let jupiter = Planet::Jupiter(Rc::clone(&sun)); - println!("reference count = {}", Rc::strong_count(&sun)); // 6 references - jupiter.details(); - - // TODO - let saturn = Planet::Saturn(Rc::new(Sun {})); - println!("reference count = {}", Rc::strong_count(&sun)); // 7 references - saturn.details(); - - // TODO - let uranus = Planet::Uranus(Rc::new(Sun {})); - println!("reference count = {}", Rc::strong_count(&sun)); // 8 references - uranus.details(); - - // TODO - let neptune = Planet::Neptune(Rc::new(Sun {})); - println!("reference count = {}", Rc::strong_count(&sun)); // 9 references - neptune.details(); - - assert_eq!(Rc::strong_count(&sun), 9); - - drop(neptune); - println!("reference count = {}", Rc::strong_count(&sun)); // 8 references - - drop(uranus); - println!("reference count = {}", Rc::strong_count(&sun)); // 7 references - - drop(saturn); - println!("reference count = {}", Rc::strong_count(&sun)); // 6 references - - drop(jupiter); - println!("reference count = {}", Rc::strong_count(&sun)); // 5 references - - drop(mars); - println!("reference count = {}", Rc::strong_count(&sun)); // 4 references - - // TODO - println!("reference count = {}", Rc::strong_count(&sun)); // 3 references - - // TODO - println!("reference count = {}", Rc::strong_count(&sun)); // 2 references - - // TODO - println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference - - assert_eq!(Rc::strong_count(&sun), 1); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rc1() { + let sun = Rc::new(Sun {}); + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + let mercury = Planet::Mercury(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + mercury.details(); + + let venus = Planet::Venus(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + venus.details(); + + let earth = Planet::Earth(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + earth.details(); + + let mars = Planet::Mars(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + mars.details(); + + let jupiter = Planet::Jupiter(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + jupiter.details(); + + // TODO + let saturn = Planet::Saturn(Rc::new(Sun {})); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + saturn.details(); + + // TODO + let uranus = Planet::Uranus(Rc::new(Sun {})); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + uranus.details(); + + // TODO + let neptune = Planet::Neptune(Rc::new(Sun {})); + println!("reference count = {}", Rc::strong_count(&sun)); // 9 references + neptune.details(); + + assert_eq!(Rc::strong_count(&sun), 9); + + drop(neptune); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + + drop(uranus); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + + drop(saturn); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + + drop(jupiter); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + + drop(mars); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + + // TODO + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + + // TODO + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + + // TODO + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + assert_eq!(Rc::strong_count(&sun), 1); + } } diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs index be1301d7..bf0b8e0d 100644 --- a/exercises/20_threads/threads1.rs +++ b/exercises/20_threads/threads1.rs @@ -1,12 +1,7 @@ -// threads1.rs -// // This program spawns multiple threads that each run for at least 250ms, and // each thread returns how much time they took to complete. The program should // wait until all the spawned threads have finished and should collect their // return values into a vector. -// -// Execute `rustlings hint threads1` or use the `hint` watch subcommand for a -// hint. use std::thread; use std::time::{Duration, Instant}; diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs index 13cb840e..2bdeba94 100644 --- a/exercises/20_threads/threads2.rs +++ b/exercises/20_threads/threads2.rs @@ -1,11 +1,6 @@ -// threads2.rs -// // Building on the last exercise, we want all of the threads to complete their // work but this time the spawned threads need to be in charge of updating a // shared value: JobStatus.jobs_completed -// -// Execute `rustlings hint threads2` or use the `hint` watch subcommand for a -// hint. use std::sync::Arc; use std::thread; diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index 35b914ac..13abc450 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -1,8 +1,3 @@ -// threads3.rs -// -// Execute `rustlings hint threads3` or use the `hint` watch subcommand for a -// hint. - use std::sync::mpsc; use std::sync::Arc; use std::thread; @@ -42,20 +37,29 @@ fn send_tx(q: Queue, tx: mpsc::Sender) -> () { }); } -#[test] fn main() { - let (tx, rx) = mpsc::channel(); - let queue = Queue::new(); - let queue_length = queue.length; - - send_tx(queue, tx); - - let mut total_received: u32 = 0; - for received in rx { - println!("Got: {}", received); - total_received += 1; - } - - println!("total numbers received: {}", total_received); - assert_eq!(total_received, queue_length) + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn threads3() { + let (tx, rx) = mpsc::channel(); + let queue = Queue::new(); + let queue_length = queue.length; + + send_tx(queue, tx); + + let mut total_received: u32 = 0; + for received in rx { + println!("Got: {}", received); + total_received += 1; + } + + println!("total numbers received: {}", total_received); + assert_eq!(total_received, queue_length) + } } diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs index 65986db0..1d415cb1 100644 --- a/exercises/21_macros/macros1.rs +++ b/exercises/21_macros/macros1.rs @@ -1,8 +1,3 @@ -// macros1.rs -// -// Execute `rustlings hint macros1` or use the `hint` watch subcommand for a -// hint. - macro_rules! my_macro { () => { println!("Check out my macro!"); diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs index b7c37fd9..f16712be 100644 --- a/exercises/21_macros/macros2.rs +++ b/exercises/21_macros/macros2.rs @@ -1,8 +1,3 @@ -// macros2.rs -// -// Execute `rustlings hint macros2` or use the `hint` watch subcommand for a -// hint. - fn main() { my_macro!(); } diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs index 92a19227..405c397a 100644 --- a/exercises/21_macros/macros3.rs +++ b/exercises/21_macros/macros3.rs @@ -1,9 +1,4 @@ -// macros3.rs -// // Make me compile, without taking the macro out of the module! -// -// Execute `rustlings hint macros3` or use the `hint` watch subcommand for a -// hint. mod macros { macro_rules! my_macro { diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs index 83a6e44f..03ece080 100644 --- a/exercises/21_macros/macros4.rs +++ b/exercises/21_macros/macros4.rs @@ -1,8 +1,3 @@ -// macros4.rs -// -// Execute `rustlings hint macros4` or use the `hint` watch subcommand for a -// hint. - #[rustfmt::skip] macro_rules! my_macro { () => { diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs index 1e0f42e2..f1eaa831 100644 --- a/exercises/22_clippy/clippy1.rs +++ b/exercises/22_clippy/clippy1.rs @@ -1,13 +1,8 @@ -// clippy1.rs -// // The Clippy tool is a collection of lints to analyze your code so you can // catch common mistakes and improve your Rust code. // // For these exercises the code will fail to compile when there are Clippy // warnings. Check Clippy's suggestions from the output to solve the exercise. -// -// Execute `rustlings hint clippy1` or use the `hint` watch subcommand for a -// hint. use std::f32; diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs index 37ac089e..c7d400d1 100644 --- a/exercises/22_clippy/clippy2.rs +++ b/exercises/22_clippy/clippy2.rs @@ -1,8 +1,3 @@ -// clippy2.rs -// -// Execute `rustlings hint clippy2` or use the `hint` watch subcommand for a -// hint. - fn main() { let mut res = 42; let option = Some(12); diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index 6a6a36b5..fd829cf6 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -1,7 +1,4 @@ -// clippy3.rs -// // Here's a couple more easy Clippy fixes, so you can see its utility. -// No hints. #[allow(unused_variables, unused_assignments)] fn main() { diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index 6fb7c2fc..c725dfde 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -1,11 +1,6 @@ -// as_ref_mut.rs -// // AsRef and AsMut allow for cheap reference-to-reference conversions. Read more // about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. -// -// Execute `rustlings hint as_ref_mut` or use the `hint` watch subcommand for a -// hint. // Obtain the number of bytes (not characters) in the given argument. // TODO: Add the AsRef trait appropriately as a trait bound. diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs index d2a1609e..9df10da9 100644 --- a/exercises/23_conversions/from_into.rs +++ b/exercises/23_conversions/from_into.rs @@ -1,11 +1,6 @@ -// from_into.rs -// // The From trait is used for value-to-value conversions. If From is implemented // correctly for a type, the Into trait should work conversely. You can read // more about it at https://doc.rust-lang.org/std/convert/trait.From.html -// -// Execute `rustlings hint from_into` or use the `hint` watch subcommand for a -// hint. #[derive(Debug)] struct Person { @@ -24,7 +19,6 @@ impl Default for Person { } } - // Your task is to complete this implementation in order for the line `let p1 = // Person::from("Mark,20")` to compile. Please note that you'll need to parse the // age component into a `usize` with something like `"4".parse::()`. The diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index ed91ca5c..58270f02 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -1,13 +1,8 @@ -// from_str.rs -// // This is similar to from_into.rs, but this time we'll implement `FromStr` and // return errors instead of falling back to a default value. Additionally, upon // implementing FromStr, you can use the `parse` method on strings to generate // an object of the implementor type. You can read more about it at // https://doc.rust-lang.org/std/str/trait.FromStr.html -// -// Execute `rustlings hint from_str` or use the `hint` watch subcommand for a -// hint. use std::num::ParseIntError; use std::str::FromStr; diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs index 23166555..da45e5a4 100644 --- a/exercises/23_conversions/try_from_into.rs +++ b/exercises/23_conversions/try_from_into.rs @@ -1,13 +1,8 @@ -// try_from_into.rs -// // TryFrom is a simple and safe type conversion that may fail in a controlled // way under some circumstances. Basically, this is the same as From. The main // difference is that this should return a Result type instead of the target // type itself. You can read more about it at // https://doc.rust-lang.org/std/convert/trait.TryFrom.html -// -// Execute `rustlings hint try_from_into` or use the `hint` watch subcommand for -// a hint. use std::convert::{TryFrom, TryInto}; diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs index 9f617ec5..94b1bb31 100644 --- a/exercises/23_conversions/using_as.rs +++ b/exercises/23_conversions/using_as.rs @@ -1,14 +1,9 @@ -// using_as.rs -// // Type casting in Rust is done via the usage of the `as` operator. Please note // that the `as` operator is not only used when type casting. It also helps with // renaming imports. // // The goal is to make sure that the division does not fail to compile and // returns the proper type. -// -// Execute `rustlings hint using_as` or use the `hint` watch subcommand for a -// hint. fn average(values: &[f64]) -> f64 { let total = values.iter().sum::(); diff --git a/exercises/quiz1.rs b/exercises/quiz1.rs index 55bc61f6..edb672ee 100644 --- a/exercises/quiz1.rs +++ b/exercises/quiz1.rs @@ -1,5 +1,3 @@ -// quiz1.rs -// // This is a quiz for the following sections: // - Variables // - Functions @@ -10,8 +8,6 @@ // - If Mary buys more than 40 apples, each apple only costs 1 rustbuck! // Write a function that calculates the price of an order of apples given the // quantity bought. -// -// No hints this time ;) // Put your function here! // fn calculate_price_of_apples { @@ -20,16 +16,21 @@ fn main() { // You can optionally experiment here. } -// Don't modify this function! -#[test] -fn verify_test() { - let price1 = calculate_price_of_apples(35); - let price2 = calculate_price_of_apples(40); - let price3 = calculate_price_of_apples(41); - let price4 = calculate_price_of_apples(65); +#[cfg(test)] +mod tests { + use super::*; - assert_eq!(70, price1); - assert_eq!(80, price2); - assert_eq!(41, price3); - assert_eq!(65, price4); + // Don't modify this test! + #[test] + fn verify_test() { + let price1 = calculate_price_of_apples(35); + let price2 = calculate_price_of_apples(40); + let price3 = calculate_price_of_apples(41); + let price4 = calculate_price_of_apples(65); + + assert_eq!(70, price1); + assert_eq!(80, price2); + assert_eq!(41, price3); + assert_eq!(65, price4); + } } diff --git a/exercises/quiz2.rs b/exercises/quiz2.rs index 1d73ab93..0a29e781 100644 --- a/exercises/quiz2.rs +++ b/exercises/quiz2.rs @@ -1,5 +1,3 @@ -// quiz2.rs -// // This is a quiz for the following sections: // - Strings // - Vecs @@ -17,8 +15,6 @@ // - The input is going to be a Vector of a 2-length tuple, // the first element is the string, the second one is the command. // - The output element is going to be a Vector of strings. -// -// No hints this time! pub enum Command { Uppercase, diff --git a/exercises/quiz3.rs b/exercises/quiz3.rs index 780e130d..f255cb5d 100644 --- a/exercises/quiz3.rs +++ b/exercises/quiz3.rs @@ -1,5 +1,3 @@ -// quiz3.rs -// // This quiz tests: // - Generics // - Traits @@ -13,8 +11,6 @@ // Make the necessary code changes in the struct ReportCard and the impl block // to support alphabetical report cards. Change the Grade in the second test to // "A+" to show that your changes allow alphabetical grades. -// -// Execute `rustlings hint quiz3` or use the `hint` watch subcommand for a hint. pub struct ReportCard { pub grade: f32, From 634e17a5abdd5b03740cfb5ab690e2b8762cf0c3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 17 Apr 2024 23:37:31 +0200 Subject: [PATCH 174/433] Fix tests --- src/exercise.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 60a65bb6..bed247eb 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -3,6 +3,7 @@ use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, fs, + path::Path, process::{Command, Output}, }; @@ -51,7 +52,7 @@ impl Exercise { cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - if DEVELOPING_OFFICIAL_RUSTLINGS { + if DEVELOPING_OFFICIAL_RUSTLINGS && Path::new("tests").exists() { cmd.arg("--manifest-path").arg("dev/Cargo.toml"); } From d64836f3170c443c6fb5f131930223831c6d724c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 01:49:32 +0200 Subject: [PATCH 175/433] Avoid an unneeded syscall --- src/embedded.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 866b12b8..eae30998 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -47,14 +47,12 @@ impl EmbeddedFlatDir { let path = Path::new(self.path); if let Err(e) = create_dir(path) { - if !path.is_dir() { + if e.kind() != io::ErrorKind::AlreadyExists { return Err(e); } } - self.readme.write_to_disk(WriteStrategy::Overwrite)?; - - Ok(()) + self.readme.write_to_disk(WriteStrategy::Overwrite) } } From 9f5be60b400f7af505770d9001731f592cb552bb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 11:20:51 +0200 Subject: [PATCH 176/433] Use git stash to reset third-party exercises --- src/exercise.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index bed247eb..7f924f90 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,10 +1,10 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, fs, path::Path, - process::{Command, Output}, + process::{Command, Output, Stdio}, }; use crate::{ @@ -88,9 +88,31 @@ impl Exercise { } pub fn reset(&self) -> Result<()> { - EMBEDDED_FILES - .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) - .with_context(|| format!("Failed to reset the exercise {self}")) + if Path::new("info.toml").exists() { + let output = Command::new("git") + .arg("stash") + .arg("push") + .arg("--") + .arg(self.path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .with_context(|| format!("Failed to run `git stash push -- {}`", self.path))?; + + if !output.status.success() { + bail!( + "`git stash push -- {}` didn't run successfully: {}", + self.path, + String::from_utf8_lossy(&output.stderr), + ); + } + } else { + EMBEDDED_FILES + .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) + .with_context(|| format!("Failed to reset the exercise {self}"))?; + } + + Ok(()) } pub fn terminal_link(&self) -> StyledContent> { From 2e9b9a9f130c89e9b2856f17c24cdab841929b28 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 11:21:39 +0200 Subject: [PATCH 177/433] Move constant --- src/app_state.rs | 24 +++++++++++++++++++++++- src/main.rs | 22 ---------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 54c02d69..2ab75268 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -9,7 +9,7 @@ use std::{ io::{Read, StdoutLock, Write}, }; -use crate::{exercise::Exercise, info_file::ExerciseInfo, FENISH_LINE}; +use crate::{exercise::Exercise, info_file::ExerciseInfo}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -275,3 +275,25 @@ All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. "; + +const FENISH_LINE: &str = "+----------------------------------------------------+ +| You made it to the Fe-nish line! | ++-------------------------- ------------------------+ + \\/\x1b[31m + ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ + ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ + ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ + ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ + ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ + ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ + ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ + ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ + ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m + +"; diff --git a/src/main.rs b/src/main.rs index fa5542af..3f3fbc1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -190,25 +190,3 @@ const PRE_INIT_MSG: &str = r" The `exercises` directory wasn't found in the current directory. If you are just starting with Rustlings, run the command `rustlings init` to initialize it."; - -const FENISH_LINE: &str = "+----------------------------------------------------+ -| You made it to the Fe-nish line! | -+-------------------------- ------------------------+ - \\/\x1b[31m - ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ - ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ - ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ - ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ - ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ - ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ - ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ - ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ - ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m - -"; From 1eac00e89ae9e0ed6969f5036e9c8c43e4435e86 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 11:28:28 +0200 Subject: [PATCH 178/433] Disable init command during development --- src/dev.rs | 12 ++++++++++-- src/main.rs | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index feca99e5..1430f11e 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,6 +1,8 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use clap::Subcommand; +use crate::DEVELOPING_OFFICIAL_RUSTLINGS; + mod check; mod init; mod update; @@ -15,7 +17,13 @@ pub enum DevCommands { impl DevCommands { pub fn run(self) -> Result<()> { match self { - DevCommands::Init => init::init().context(INIT_ERR), + DevCommands::Init => { + if DEVELOPING_OFFICIAL_RUSTLINGS { + bail!("Disabled while developing the official Rustlings"); + } + + init::init().context(INIT_ERR) + } DevCommands::Check => check::check(), DevCommands::Update => update::update(), } diff --git a/src/main.rs b/src/main.rs index 3f3fbc1d..d40f978c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,10 @@ fn main() -> Result<()> { match args.command { Some(Subcommands::Init) => { + if DEVELOPING_OFFICIAL_RUSTLINGS { + bail!("Disabled while developing the official Rustlings"); + } + return init::init().context("Initialization failed"); } Some(Subcommands::Dev(dev_command)) => return dev_command.run(), From 2566f9aaf674458f6faff7e8aaf77bb108b7d56c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 11:31:08 +0200 Subject: [PATCH 179/433] Place mods under all imports --- src/list.rs | 4 ++-- src/main.rs | 4 ++-- src/watch.rs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/list.rs b/src/list.rs index 2bb813d8..790c02fe 100644 --- a/src/list.rs +++ b/src/list.rs @@ -7,12 +7,12 @@ use crossterm::{ use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; -mod state; - use crate::app_state::AppState; use self::state::{Filter, UiState}; +mod state; + pub fn list(app_state: &mut AppState) -> Result<()> { let mut stdout = io::stdout().lock(); stdout.execute(EnterAlternateScreen)?; diff --git a/src/main.rs b/src/main.rs index d40f978c..c8940af2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ use std::{ process::exit, }; +use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; + mod app_state; mod dev; mod embedded; @@ -22,8 +24,6 @@ mod progress_bar; mod run; mod watch; -use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; - const CURRENT_FORMAT_VERSION: u8 = 1; const DEVELOPING_OFFICIAL_RUSTLINGS: bool = { #[allow(unused_assignments, unused_mut)] diff --git a/src/watch.rs b/src/watch.rs index d20e552e..5ebe526f 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -11,10 +11,6 @@ use std::{ time::Duration, }; -mod notify_event; -mod state; -mod terminal_event; - use crate::app_state::{AppState, ExercisesProgress}; use self::{ @@ -23,6 +19,10 @@ use self::{ terminal_event::{terminal_event_handler, InputEvent}, }; +mod notify_event; +mod state; +mod terminal_event; + enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, From f04089b8bc724d41dcd26117c85f32328b5eb597 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 11:40:54 +0200 Subject: [PATCH 180/433] Only take a reference --- src/dev/check.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index d2e5fe14..e95eb3c6 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -79,7 +79,7 @@ fn unexpected_file(path: &Path) -> Error { anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display()) } -fn check_exercise_dir_files(info_file_paths: hashbrown::HashSet) -> Result<()> { +fn check_exercise_dir_files(info_file_paths: &hashbrown::HashSet) -> Result<()> { for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { let entry = entry.context("Failed to read the `exercises` directory")?; @@ -131,7 +131,7 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { } let info_file_paths = check_info_file_exercises(info_file)?; - check_exercise_dir_files(info_file_paths)?; + check_exercise_dir_files(&info_file_paths)?; Ok(()) } From 01e6732e4d920d9a1859e05fa28382e4307571af Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 12:41:17 +0200 Subject: [PATCH 181/433] Improve resetting --- src/app_state.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++- src/exercise.rs | 38 +++---------------------------- src/list/state.rs | 13 +++++------ src/main.rs | 6 ++--- 4 files changed, 67 insertions(+), 47 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 2ab75268..98687818 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,9 +7,15 @@ use crossterm::{ use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, + path::Path, + process::{Command, Stdio}, }; -use crate::{exercise::Exercise, info_file::ExerciseInfo}; +use crate::{ + embedded::{WriteStrategy, EMBEDDED_FILES}, + exercise::Exercise, + info_file::ExerciseInfo, +}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -31,6 +37,7 @@ pub struct AppState { n_done: u16, final_message: String, file_buf: Vec, + official_exercises: bool, } impl AppState { @@ -111,6 +118,7 @@ impl AppState { n_done: 0, final_message, file_buf: Vec::with_capacity(2048), + official_exercises: !Path::new("info.toml").exists(), }; let state_file_status = slf.update_from_file(); @@ -172,6 +180,53 @@ impl AppState { Ok(()) } + fn reset_path(&self, path: &str) -> Result<()> { + if self.official_exercises { + return EMBEDDED_FILES + .write_exercise_to_disk(path, WriteStrategy::Overwrite) + .with_context(|| format!("Failed to reset the exercise {path}")); + } + + let output = Command::new("git") + .arg("stash") + .arg("push") + .arg("--") + .arg(path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .with_context(|| format!("Failed to run `git stash push -- {path}`"))?; + + if !output.status.success() { + bail!( + "`git stash push -- {path}` didn't run successfully: {}", + String::from_utf8_lossy(&output.stderr), + ); + } + + Ok(()) + } + + pub fn reset_current_exercise(&mut self) -> Result<&'static str> { + let path = self.current_exercise().path; + self.set_pending(self.current_exercise_ind)?; + self.reset_path(path)?; + + Ok(path) + } + + pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> { + if exercise_ind >= self.exercises.len() { + bail!(BAD_INDEX_ERR); + } + + let path = self.exercises[exercise_ind].path; + self.set_pending(exercise_ind)?; + self.reset_path(path)?; + + Ok(path) + } + fn next_pending_exercise_ind(&self) -> Option { if self.current_exercise_ind == self.exercises.len() - 1 { // The last exercise is done. diff --git a/src/exercise.rs b/src/exercise.rs index 7f924f90..eb7b725e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,17 +1,13 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, fs, path::Path, - process::{Command, Output, Stdio}, + process::{Command, Output}, }; -use crate::{ - embedded::{WriteStrategy, EMBEDDED_FILES}, - info_file::Mode, - DEVELOPING_OFFICIAL_RUSTLINGS, -}; +use crate::{info_file::Mode, DEVELOPING_OFFICIAL_RUSTLINGS}; pub struct TerminalFileLink<'a> { path: &'a str, @@ -87,34 +83,6 @@ impl Exercise { } } - pub fn reset(&self) -> Result<()> { - if Path::new("info.toml").exists() { - let output = Command::new("git") - .arg("stash") - .arg("push") - .arg("--") - .arg(self.path) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .with_context(|| format!("Failed to run `git stash push -- {}`", self.path))?; - - if !output.status.success() { - bail!( - "`git stash push -- {}` didn't run successfully: {}", - self.path, - String::from_utf8_lossy(&output.stderr), - ); - } - } else { - EMBEDDED_FILES - .write_exercise_to_disk(self.path, WriteStrategy::Overwrite) - .with_context(|| format!("Failed to reset the exercise {self}"))?; - } - - Ok(()) - } - pub fn terminal_link(&self) -> StyledContent> { style(TerminalFileLink { path: self.path }) .underlined() diff --git a/src/list/state.rs b/src/list/state.rs index 2a1fef18..0253bb97 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -217,23 +217,22 @@ impl<'a> UiState<'a> { return Ok(self); }; - let (ind, exercise) = self + let ind = self .app_state .exercises() .iter() .enumerate() .filter_map(|(ind, exercise)| match self.filter { - Filter::Done => exercise.done.then_some((ind, exercise)), - Filter::Pending => (!exercise.done).then_some((ind, exercise)), - Filter::None => Some((ind, exercise)), + Filter::Done => exercise.done.then_some(ind), + Filter::Pending => (!exercise.done).then_some(ind), + Filter::None => Some(ind), }) .nth(selected) .context("Invalid selection index")?; - exercise.reset()?; + let exercise_path = self.app_state.reset_exercise_by_ind(ind)?; self.message - .write_fmt(format_args!("The exercise {exercise} has been reset!"))?; - self.app_state.set_pending(ind)?; + .write_fmt(format_args!("The exercise {exercise_path} has been reset"))?; Ok(self.with_updated_rows()) } diff --git a/src/main.rs b/src/main.rs index c8940af2..04697379 100644 --- a/src/main.rs +++ b/src/main.rs @@ -158,10 +158,8 @@ fn main() -> Result<()> { } Some(Subcommands::Reset { name }) => { app_state.set_current_exercise_by_name(&name)?; - let exercise = app_state.current_exercise(); - exercise.reset()?; - println!("The exercise {exercise} has been reset!"); - app_state.set_pending(app_state.current_exercise_ind())?; + let exercise_path = app_state.reset_current_exercise()?; + println!("The exercise {exercise_path} has been reset"); } Some(Subcommands::Hint { name }) => { app_state.set_current_exercise_by_name(&name)?; From cc35a8431ff58502cdca08393e28578f6a4e9b53 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 13:07:07 +0200 Subject: [PATCH 182/433] Remove "All-Contributors" --- .all-contributorsrc | 2743 ------------------------------------------- AUTHORS.md | 397 ------- 2 files changed, 3140 deletions(-) delete mode 100644 .all-contributorsrc delete mode 100644 AUTHORS.md diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index 36952f7e..00000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,2743 +0,0 @@ -{ - "files": [ - "AUTHORS.md" - ], - "imageSize": 100, - "commit": false, - "contributors": [ - { - "login": "carols10cents", - "name": "Carol (Nichols || Goulding)", - "avatar_url": "https://avatars2.githubusercontent.com/u/193874?v=4", - "profile": "http://carol-nichols.com", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "QuietMisdreavus", - "name": "QuietMisdreavus", - "avatar_url": "https://avatars2.githubusercontent.com/u/5217170?v=4", - "profile": "https://twitter.com/QuietMisdreavus", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "robertlugg", - "name": "Robert M Lugg", - "avatar_url": "https://avatars0.githubusercontent.com/u/6054540?v=4", - "profile": "https://github.com/robertlugg", - "contributions": [ - "content" - ] - }, - { - "login": "hynek", - "name": "Hynek Schlawack", - "avatar_url": "https://avatars3.githubusercontent.com/u/41240?v=4", - "profile": "https://hynek.me/about/", - "contributions": [ - "code" - ] - }, - { - "login": "spacekookie", - "name": "Katharina Fey", - "avatar_url": "https://avatars0.githubusercontent.com/u/7669898?v=4", - "profile": "https://spacekookie.de", - "contributions": [ - "code" - ] - }, - { - "login": "lukabavdaz", - "name": "lukabavdaz", - "avatar_url": "https://avatars0.githubusercontent.com/u/9624558?v=4", - "profile": "https://github.com/lukabavdaz", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "evestera", - "name": "Erik Vesteraas", - "avatar_url": "https://avatars2.githubusercontent.com/u/4187449?v=4", - "profile": "http://vestera.as", - "contributions": [ - "code" - ] - }, - { - "login": "Delet0r", - "name": "delet0r", - "avatar_url": "https://avatars1.githubusercontent.com/u/23195618?v=4", - "profile": "https://github.com/Delet0r", - "contributions": [ - "code" - ] - }, - { - "login": "shaunbennett", - "name": "Shaun Bennett", - "avatar_url": "https://avatars1.githubusercontent.com/u/10522375?v=4", - "profile": "http://phinary.ca", - "contributions": [ - "code" - ] - }, - { - "login": "abagshaw", - "name": "Andrew Bagshaw", - "avatar_url": "https://avatars2.githubusercontent.com/u/8594541?v=4", - "profile": "https://github.com/abagshaw", - "contributions": [ - "code" - ] - }, - { - "login": "kisom", - "name": "Kyle Isom", - "avatar_url": "https://avatars2.githubusercontent.com/u/175578?v=4", - "profile": "https://ai6ua.net/", - "contributions": [ - "code" - ] - }, - { - "login": "ColinPitrat", - "name": "Colin Pitrat", - "avatar_url": "https://avatars3.githubusercontent.com/u/1541863?v=4", - "profile": "https://github.com/ColinPitrat", - "contributions": [ - "code" - ] - }, - { - "login": "zacanger", - "name": "Zac Anger", - "avatar_url": "https://avatars3.githubusercontent.com/u/12520493?v=4", - "profile": "https://zacanger.com", - "contributions": [ - "code" - ] - }, - { - "login": "mgeier", - "name": "Matthias Geier", - "avatar_url": "https://avatars1.githubusercontent.com/u/705404?v=4", - "profile": "https://github.com/mgeier", - "contributions": [ - "code" - ] - }, - { - "login": "cjpearce", - "name": "Chris Pearce", - "avatar_url": "https://avatars1.githubusercontent.com/u/3453268?v=4", - "profile": "https://github.com/cjpearce", - "contributions": [ - "code" - ] - }, - { - "login": "yvan-sraka", - "name": "Yvan Sraka", - "avatar_url": "https://avatars2.githubusercontent.com/u/705213?v=4", - "profile": "https://yvan-sraka.github.io", - "contributions": [ - "code" - ] - }, - { - "login": "dendi239", - "name": "Denys Smirnov", - "avatar_url": "https://avatars3.githubusercontent.com/u/16478650?v=4", - "profile": "https://github.com/dendi239", - "contributions": [ - "code" - ] - }, - { - "login": "eddyp", - "name": "eddyp", - "avatar_url": "https://avatars2.githubusercontent.com/u/123772?v=4", - "profile": "https://github.com/eddyp", - "contributions": [ - "code" - ] - }, - { - "login": "briankung", - "name": "Brian Kung", - "avatar_url": "https://avatars1.githubusercontent.com/u/2836167?v=4", - "profile": "http://about.me/BrianKung", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "miller-time", - "name": "Russell", - "avatar_url": "https://avatars3.githubusercontent.com/u/281039?v=4", - "profile": "https://rcousineau.gitlab.io", - "contributions": [ - "code" - ] - }, - { - "login": "danwilhelm", - "name": "Dan Wilhelm", - "avatar_url": "https://avatars3.githubusercontent.com/u/6137185?v=4", - "profile": "http://danwilhelm.com", - "contributions": [ - "doc" - ] - }, - { - "login": "Jesse-Cameron", - "name": "Jesse", - "avatar_url": "https://avatars3.githubusercontent.com/u/3723654?v=4", - "profile": "https://github.com/Jesse-Cameron", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "MrFroop", - "name": "Fredrik Jambrén", - "avatar_url": "https://avatars3.githubusercontent.com/u/196700?v=4", - "profile": "https://github.com/MrFroop", - "contributions": [ - "code" - ] - }, - { - "login": "petemcfarlane", - "name": "Pete McFarlane", - "avatar_url": "https://avatars3.githubusercontent.com/u/3472717?v=4", - "profile": "https://github.com/petemcfarlane", - "contributions": [ - "content" - ] - }, - { - "login": "nkanderson", - "name": "nkanderson", - "avatar_url": "https://avatars0.githubusercontent.com/u/4128825?v=4", - "profile": "https://github.com/nkanderson", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "ajaxm", - "name": "Ajax M", - "avatar_url": "https://avatars0.githubusercontent.com/u/13360138?v=4", - "profile": "https://github.com/ajaxm", - "contributions": [ - "doc" - ] - }, - { - "login": "Dylnuge", - "name": "Dylan Nugent", - "avatar_url": "https://avatars2.githubusercontent.com/u/118624?v=4", - "profile": "https://dylnuge.com", - "contributions": [ - "content" - ] - }, - { - "login": "vyaslav", - "name": "vyaslav", - "avatar_url": "https://avatars0.githubusercontent.com/u/1385427?v=4", - "profile": "https://github.com/vyaslav", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "gdoenlen", - "name": "George", - "avatar_url": "https://avatars1.githubusercontent.com/u/17297466?v=4", - "profile": "https://join.sfxd.org", - "contributions": [ - "code" - ] - }, - { - "login": "nyxtom", - "name": "Thomas Holloway", - "avatar_url": "https://avatars2.githubusercontent.com/u/222763?v=4", - "profile": "https://github.com/nyxtom", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "workingjubilee", - "name": "Jubilee", - "avatar_url": "https://avatars1.githubusercontent.com/u/46493976?v=4", - "profile": "https://github.com/workingjubilee", - "contributions": [ - "code" - ] - }, - { - "login": "WofWca", - "name": "WofWca", - "avatar_url": "https://avatars1.githubusercontent.com/u/39462442?v=4", - "profile": "https://github.com/WofWca", - "contributions": [ - "code" - ] - }, - { - "login": "jrvidal", - "name": "Roberto Vidal", - "avatar_url": "https://avatars0.githubusercontent.com/u/1636604?v=4", - "profile": "https://github.com/jrvidal", - "contributions": [ - "code", - "doc", - "ideas", - "maintenance" - ] - }, - { - "login": "jensim", - "name": "Jens", - "avatar_url": "https://avatars0.githubusercontent.com/u/3663856?v=4", - "profile": "https://github.com/jensim", - "contributions": [ - "doc" - ] - }, - { - "login": "rahatarmanahmed", - "name": "Rahat Ahmed", - "avatar_url": "https://avatars3.githubusercontent.com/u/3174006?v=4", - "profile": "http://rahatah.me/d", - "contributions": [ - "doc" - ] - }, - { - "login": "AbdouSeck", - "name": "Abdou Seck", - "avatar_url": "https://avatars2.githubusercontent.com/u/6490055?v=4", - "profile": "https://github.com/AbdouSeck", - "contributions": [ - "code", - "content", - "review" - ] - }, - { - "login": "codehearts", - "name": "Katie", - "avatar_url": "https://avatars0.githubusercontent.com/u/2885412?v=4", - "profile": "https://codehearts.com", - "contributions": [ - "code" - ] - }, - { - "login": "Socratides", - "name": "Socrates", - "avatar_url": "https://avatars3.githubusercontent.com/u/27732983?v=4", - "profile": "https://github.com/Socratides", - "contributions": [ - "doc" - ] - }, - { - "login": "gnodarse", - "name": "gnodarse", - "avatar_url": "https://avatars3.githubusercontent.com/u/46761795?v=4", - "profile": "https://github.com/gnodarse", - "contributions": [ - "content" - ] - }, - { - "login": "harrisonmetz", - "name": "Harrison Metzger", - "avatar_url": "https://avatars1.githubusercontent.com/u/7883408?v=4", - "profile": "https://github.com/harrisonmetz", - "contributions": [ - "code" - ] - }, - { - "login": "TorbenJ", - "name": "Torben Jonas", - "avatar_url": "https://avatars2.githubusercontent.com/u/9077102?v=4", - "profile": "https://github.com/TorbenJ", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "pbx", - "name": "Paul Bissex", - "avatar_url": "https://avatars0.githubusercontent.com/u/641?v=4", - "profile": "http://paulbissex.com/", - "contributions": [ - "doc" - ] - }, - { - "login": "sjmann", - "name": "Steven Mann", - "avatar_url": "https://avatars0.githubusercontent.com/u/6589896?v=4", - "profile": "https://github.com/sjmann", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "Tarnadas", - "name": "Mario Reder", - "avatar_url": "https://avatars2.githubusercontent.com/u/5855071?v=4", - "profile": "https://smmdb.net/", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "sl4m", - "name": "skim", - "avatar_url": "https://avatars0.githubusercontent.com/u/47347?v=4", - "profile": "https://keybase.io/skim", - "contributions": [ - "code" - ] - }, - { - "login": "sanjaykdragon", - "name": "Sanjay K", - "avatar_url": "https://avatars1.githubusercontent.com/u/10261698?v=4", - "profile": "https://github.com/sanjaykdragon", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "crodjer", - "name": "Rohan Jain", - "avatar_url": "https://avatars1.githubusercontent.com/u/343499?v=4", - "profile": "http://www.rohanjain.in", - "contributions": [ - "code" - ] - }, - { - "login": "saidaspen", - "name": "Said Aspen", - "avatar_url": "https://avatars1.githubusercontent.com/u/7727687?v=4", - "profile": "https://www.saidaspen.se", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "uce", - "name": "Ufuk Celebi", - "avatar_url": "https://avatars3.githubusercontent.com/u/1756620?v=4", - "profile": "https://github.com/uce", - "contributions": [ - "code" - ] - }, - { - "login": "lebedevsergey", - "name": "lebedevsergey", - "avatar_url": "https://avatars2.githubusercontent.com/u/7325764?v=4", - "profile": "https://github.com/lebedevsergey", - "contributions": [ - "doc" - ] - }, - { - "login": "avrong", - "name": "Aleksei Trifonov", - "avatar_url": "https://avatars2.githubusercontent.com/u/6342851?v=4", - "profile": "https://github.com/avrong", - "contributions": [ - "content" - ] - }, - { - "login": "Darrenmeehan", - "name": "Darren Meehan", - "avatar_url": "https://avatars2.githubusercontent.com/u/411136?v=4", - "profile": "https://drn.ie", - "contributions": [ - "content" - ] - }, - { - "login": "jihchi", - "name": "Jihchi Lee", - "avatar_url": "https://avatars1.githubusercontent.com/u/87983?v=4", - "profile": "https://github.com/jihchi", - "contributions": [ - "content" - ] - }, - { - "login": "bertonha", - "name": "Christofer Bertonha", - "avatar_url": "https://avatars3.githubusercontent.com/u/1225902?v=4", - "profile": "https://github.com/bertonha", - "contributions": [ - "content" - ] - }, - { - "login": "apatniv", - "name": "Vivek Bharath Akupatni", - "avatar_url": "https://avatars2.githubusercontent.com/u/22565917?v=4", - "profile": "https://github.com/apatniv", - "contributions": [ - "code", - "test" - ] - }, - { - "login": "DiD92", - "name": "Dídac Sementé Fernández", - "avatar_url": "https://avatars3.githubusercontent.com/u/6002416?v=4", - "profile": "https://github.com/DiD92", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "wrobstory", - "name": "Rob Story", - "avatar_url": "https://avatars3.githubusercontent.com/u/2601457?v=4", - "profile": "https://github.com/wrobstory", - "contributions": [ - "code" - ] - }, - { - "login": "siobhanjacobson", - "name": "Siobhan Jacobson", - "avatar_url": "https://avatars2.githubusercontent.com/u/28983835?v=4", - "profile": "https://github.com/siobhanjacobson", - "contributions": [ - "code" - ] - }, - { - "login": "EvanCarroll", - "name": "Evan Carroll", - "avatar_url": "https://avatars2.githubusercontent.com/u/19922?v=4", - "profile": "https://www.linkedin.com/in/evancarroll/", - "contributions": [ - "content" - ] - }, - { - "login": "jmahmood", - "name": "Jawaad Mahmood", - "avatar_url": "https://avatars3.githubusercontent.com/u/95606?v=4", - "profile": "http://www.jawaadmahmood.com", - "contributions": [ - "content" - ] - }, - { - "login": "GaurangTandon", - "name": "Gaurang Tandon", - "avatar_url": "https://avatars1.githubusercontent.com/u/6308683?v=4", - "profile": "https://github.com/GaurangTandon", - "contributions": [ - "content" - ] - }, - { - "login": "dev-cyprium", - "name": "Stefan Kupresak", - "avatar_url": "https://avatars1.githubusercontent.com/u/6002628?v=4", - "profile": "https://github.com/dev-cyprium", - "contributions": [ - "content" - ] - }, - { - "login": "greg-el", - "name": "Greg Leonard", - "avatar_url": "https://avatars3.githubusercontent.com/u/45019882?v=4", - "profile": "https://github.com/greg-el", - "contributions": [ - "content" - ] - }, - { - "login": "ryanpcmcquen", - "name": "Ryan McQuen", - "avatar_url": "https://avatars3.githubusercontent.com/u/772937?v=4", - "profile": "https://ryanpcmcquen.org", - "contributions": [ - "code" - ] - }, - { - "login": "AnnikaCodes", - "name": "Annika", - "avatar_url": "https://avatars3.githubusercontent.com/u/56906084?v=4", - "profile": "https://github.com/AnnikaCodes", - "contributions": [ - "review" - ] - }, - { - "login": "darnuria", - "name": "Axel Viala", - "avatar_url": "https://avatars1.githubusercontent.com/u/2827553?v=4", - "profile": "https://darnuria.eu", - "contributions": [ - "code" - ] - }, - { - "login": "sazid", - "name": "Mohammed Sazid Al Rashid", - "avatar_url": "https://avatars1.githubusercontent.com/u/2370167?v=4", - "profile": "https://sazid.github.io", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "seeplusplus", - "name": "Caleb Webber", - "avatar_url": "https://avatars1.githubusercontent.com/u/17479099?v=4", - "profile": "https://codingthemsoftly.com", - "contributions": [ - "maintenance" - ] - }, - { - "login": "pcn", - "name": "Peter N", - "avatar_url": "https://avatars2.githubusercontent.com/u/1056756?v=4", - "profile": "https://github.com/pcn", - "contributions": [ - "maintenance" - ] - }, - { - "login": "seancad", - "name": "seancad", - "avatar_url": "https://avatars1.githubusercontent.com/u/47405611?v=4", - "profile": "https://github.com/seancad", - "contributions": [ - "maintenance" - ] - }, - { - "login": "wsh", - "name": "Will Hayworth", - "avatar_url": "https://avatars3.githubusercontent.com/u/181174?v=4", - "profile": "http://willhayworth.com", - "contributions": [ - "content" - ] - }, - { - "login": "chrizel", - "name": "Christian Zeller", - "avatar_url": "https://avatars3.githubusercontent.com/u/20802?v=4", - "profile": "https://github.com/chrizel", - "contributions": [ - "content" - ] - }, - { - "login": "jfchevrette", - "name": "Jean-Francois Chevrette", - "avatar_url": "https://avatars.githubusercontent.com/u/3001?v=4", - "profile": "https://github.com/jfchevrette", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "jbaber", - "name": "John Baber-Lucero", - "avatar_url": "https://avatars.githubusercontent.com/u/1908117?v=4", - "profile": "https://github.com/jbaber", - "contributions": [ - "content" - ] - }, - { - "login": "tal-zvon", - "name": "Tal", - "avatar_url": "https://avatars.githubusercontent.com/u/3195851?v=4", - "profile": "https://github.com/tal-zvon", - "contributions": [ - "content" - ] - }, - { - "login": "apogeeoak", - "name": "apogeeoak", - "avatar_url": "https://avatars.githubusercontent.com/u/59737221?v=4", - "profile": "https://github.com/apogeeoak", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "Crell", - "name": "Larry Garfield", - "avatar_url": "https://avatars.githubusercontent.com/u/254863?v=4", - "profile": "http://www.garfieldtech.com/", - "contributions": [ - "content" - ] - }, - { - "login": "circumspect", - "name": "circumspect", - "avatar_url": "https://avatars.githubusercontent.com/u/40770208?v=4", - "profile": "https://github.com/circumspect", - "contributions": [ - "content" - ] - }, - { - "login": "cjwyett", - "name": "Cyrus Wyett", - "avatar_url": "https://avatars.githubusercontent.com/u/34195737?v=4", - "profile": "https://github.com/cjwyett", - "contributions": [ - "content" - ] - }, - { - "login": "cadolphs", - "name": "cadolphs", - "avatar_url": "https://avatars.githubusercontent.com/u/13894820?v=4", - "profile": "https://github.com/cadolphs", - "contributions": [ - "code" - ] - }, - { - "login": "hpwxf", - "name": "Pascal H.", - "avatar_url": "https://avatars.githubusercontent.com/u/26146722?v=4", - "profile": "https://www.haveneer.com", - "contributions": [ - "content" - ] - }, - { - "login": "chapeupreto", - "name": "Rod Elias", - "avatar_url": "https://avatars.githubusercontent.com/u/834048?v=4", - "profile": "https://twitter.com/chapeupreto", - "contributions": [ - "content" - ] - }, - { - "login": "blerchy", - "name": "Matt Lebl", - "avatar_url": "https://avatars.githubusercontent.com/u/2555355?v=4", - "profile": "https://github.com/blerchy", - "contributions": [ - "code" - ] - }, - { - "login": "flakolefluk", - "name": "Ignacio Le Fluk", - "avatar_url": "https://avatars.githubusercontent.com/u/11986564?v=4", - "profile": "http://flakolefluk.dev", - "contributions": [ - "content" - ] - }, - { - "login": "tlyu", - "name": "Taylor Yu", - "avatar_url": "https://avatars.githubusercontent.com/u/431873?v=4", - "profile": "https://github.com/tlyu", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "Zerotask", - "name": "Patrick Hintermayer", - "avatar_url": "https://avatars.githubusercontent.com/u/20150243?v=4", - "profile": "https://zerotask.github.io", - "contributions": [ - "code" - ] - }, - { - "login": "arthas168", - "name": "Pete Pavlovski", - "avatar_url": "https://avatars.githubusercontent.com/u/32264020?v=4", - "profile": "https://petkopavlovski.com/", - "contributions": [ - "content" - ] - }, - { - "login": "k12ish", - "name": "k12ish", - "avatar_url": "https://avatars.githubusercontent.com/u/45272873?v=4", - "profile": "https://github.com/k12ish", - "contributions": [ - "content" - ] - }, - { - "login": "hongshaoyang", - "name": "Shao Yang Hong", - "avatar_url": "https://avatars.githubusercontent.com/u/19281800?v=4", - "profile": "https://github.com/hongshaoyang", - "contributions": [ - "content" - ] - }, - { - "login": "bmacer", - "name": "Brandon Macer", - "avatar_url": "https://avatars.githubusercontent.com/u/13931806?v=4", - "profile": "https://github.com/bmacer", - "contributions": [ - "content" - ] - }, - { - "login": "stoiandan", - "name": "Stoian Dan", - "avatar_url": "https://avatars.githubusercontent.com/u/10388612?v=4", - "profile": "https://github.com/stoiandan", - "contributions": [ - "content" - ] - }, - { - "login": "PiDelport", - "name": "Pi Delport", - "avatar_url": "https://avatars.githubusercontent.com/u/630271?v=4", - "profile": "https://about.me/pjdelport", - "contributions": [ - "content" - ] - }, - { - "login": "sateeshkumarb", - "name": "Sateesh ", - "avatar_url": "https://avatars.githubusercontent.com/u/429263?v=4", - "profile": "https://github.com/sateeshkumarb", - "contributions": [ - "code", - "content" - ] - }, - { - "login": "kayuapi", - "name": "ZC", - "avatar_url": "https://avatars.githubusercontent.com/u/10304328?v=4", - "profile": "https://github.com/kayuapi", - "contributions": [ - "content" - ] - }, - { - "login": "hyperparabolic", - "name": "hyperparabolic", - "avatar_url": "https://avatars.githubusercontent.com/u/12348474?v=4", - "profile": "https://github.com/hyperparabolic", - "contributions": [ - "code" - ] - }, - { - "login": "kolbma", - "name": "arlecchino", - "avatar_url": "https://avatars.githubusercontent.com/u/5228369?v=4", - "profile": "https://www.net4visions.at", - "contributions": [ - "doc" - ] - }, - { - "login": "jazzplato", - "name": "Richthofen", - "avatar_url": "https://avatars.githubusercontent.com/u/7576730?v=4", - "profile": "https://richthofen.io/", - "contributions": [ - "code" - ] - }, - { - "login": "cseltol", - "name": "Ivan Nerazumov", - "avatar_url": "https://avatars.githubusercontent.com/u/64264529?v=4", - "profile": "https://github.com/cseltol", - "contributions": [ - "doc" - ] - }, - { - "login": "lauralindzey", - "name": "lauralindzey", - "avatar_url": "https://avatars.githubusercontent.com/u/65185744?v=4", - "profile": "https://github.com/lauralindzey", - "contributions": [ - "doc" - ] - }, - { - "login": "sinharaksh1t", - "name": "Rakshit Sinha", - "avatar_url": "https://avatars.githubusercontent.com/u/28585848?v=4", - "profile": "https://github.com/sinharaksh1t", - "contributions": [ - "content" - ] - }, - { - "login": "dbednar230", - "name": "Damian", - "avatar_url": "https://avatars.githubusercontent.com/u/54457902?v=4", - "profile": "https://github.com/dbednar230", - "contributions": [ - "content" - ] - }, - { - "login": "benarmstead", - "name": "Ben Armstead", - "avatar_url": "https://avatars.githubusercontent.com/u/70973680?v=4", - "profile": "https://benarmstead.co.uk", - "contributions": [ - "code" - ] - }, - { - "login": "anuk909", - "name": "anuk909", - "avatar_url": "https://avatars.githubusercontent.com/u/34924662?v=4", - "profile": "https://github.com/anuk909", - "contributions": [ - "content", - "code" - ] - }, - { - "login": "granddaifuku", - "name": "granddaifuku", - "avatar_url": "https://avatars.githubusercontent.com/u/49578068?v=4", - "profile": "https://granddaifuku.com/", - "contributions": [ - "content" - ] - }, - { - "login": "Weilet", - "name": "Weilet", - "avatar_url": "https://avatars.githubusercontent.com/u/32561597?v=4", - "profile": "https://weilet.me", - "contributions": [ - "content" - ] - }, - { - "login": "Millione", - "name": "LIU JIE", - "avatar_url": "https://avatars.githubusercontent.com/u/38575932?v=4", - "profile": "https://github.com/Millione", - "contributions": [ - "content" - ] - }, - { - "login": "abusch", - "name": "Antoine Büsch", - "avatar_url": "https://avatars.githubusercontent.com/u/506344?v=4", - "profile": "https://github.com/abusch", - "contributions": [ - "code" - ] - }, - { - "login": "frogtd", - "name": "frogtd", - "avatar_url": "https://avatars.githubusercontent.com/u/31412003?v=4", - "profile": "https://frogtd.com/", - "contributions": [ - "content" - ] - }, - { - "login": "EmisonLu", - "name": "Zhenghao Lu", - "avatar_url": "https://avatars.githubusercontent.com/u/54395432?v=4", - "profile": "https://github.com/EmisonLu", - "contributions": [ - "content" - ] - }, - { - "login": "fredr", - "name": "Fredrik Enestad", - "avatar_url": "https://avatars.githubusercontent.com/u/762956?v=4", - "profile": "https://soundtrackyourbrand.com", - "contributions": [ - "content" - ] - }, - { - "login": "xuesongbj", - "name": "xuesong", - "avatar_url": "https://avatars.githubusercontent.com/u/18476085?v=4", - "profile": "http://xuesong.pydevops.com", - "contributions": [ - "content" - ] - }, - { - "login": "MpdWalsh", - "name": "Michael Walsh", - "avatar_url": "https://avatars.githubusercontent.com/u/48160144?v=4", - "profile": "https://github.com/MpdWalsh", - "contributions": [ - "code" - ] - }, - { - "login": "alirezaghey", - "name": "alirezaghey", - "avatar_url": "https://avatars.githubusercontent.com/u/26653424?v=4", - "profile": "https://github.com/alirezaghey", - "contributions": [ - "content" - ] - }, - { - "login": "frvannes16", - "name": "Franklin van Nes", - "avatar_url": "https://avatars.githubusercontent.com/u/3188475?v=4", - "profile": "https://github.com/frvannes16", - "contributions": [ - "code" - ] - }, - { - "login": "nekonako", - "name": "nekonako", - "avatar_url": "https://avatars.githubusercontent.com/u/46141275?v=4", - "profile": "https://nekonako.github.io", - "contributions": [ - "code" - ] - }, - { - "login": "tan-zx", - "name": "ZX", - "avatar_url": "https://avatars.githubusercontent.com/u/67887489?v=4", - "profile": "https://github.com/tan-zx", - "contributions": [ - "content" - ] - }, - { - "login": "sundevilyang", - "name": "Yang Wen", - "avatar_url": "https://avatars.githubusercontent.com/u/1499214?v=4", - "profile": "https://github.com/sundevilyang", - "contributions": [ - "content" - ] - }, - { - "login": "highb", - "name": "Brandon High", - "avatar_url": "https://avatars.githubusercontent.com/u/759848?v=4", - "profile": "https://brandon-high.com", - "contributions": [ - "doc" - ] - }, - { - "login": "x-hgg-x", - "name": "x-hgg-x", - "avatar_url": "https://avatars.githubusercontent.com/u/39058530?v=4", - "profile": "https://github.com/x-hgg-x", - "contributions": [ - "code" - ] - }, - { - "login": "KisaragiEffective", - "name": "Kisaragi", - "avatar_url": "https://avatars.githubusercontent.com/u/48310258?v=4", - "profile": "http://kisaragieffective.github.io", - "contributions": [ - "doc" - ] - }, - { - "login": "Kallu-A", - "name": "Lucas Aries", - "avatar_url": "https://avatars.githubusercontent.com/u/73198738?v=4", - "profile": "https://github.com/Kallu-A", - "contributions": [ - "content" - ] - }, - { - "login": "ragreenburg", - "name": "ragreenburg", - "avatar_url": "https://avatars.githubusercontent.com/u/24358100?v=4", - "profile": "https://github.com/ragreenburg", - "contributions": [ - "content" - ] - }, - { - "login": "stevenfukase", - "name": "stevenfukase", - "avatar_url": "https://avatars.githubusercontent.com/u/66785624?v=4", - "profile": "https://github.com/stevenfukase", - "contributions": [ - "content" - ] - }, - { - "login": "J-S-Kim", - "name": "J-S-Kim", - "avatar_url": "https://avatars.githubusercontent.com/u/17569303?v=4", - "profile": "https://github.com/J-S-Kim", - "contributions": [ - "content" - ] - }, - { - "login": "Fointard", - "name": "Fointard", - "avatar_url": "https://avatars.githubusercontent.com/u/9333398?v=4", - "profile": "https://github.com/Fointard", - "contributions": [ - "content" - ] - }, - { - "login": "rytheo", - "name": "Ryan Lowe", - "avatar_url": "https://avatars.githubusercontent.com/u/22184325?v=4", - "profile": "https://github.com/rytheo", - "contributions": [ - "code" - ] - }, - { - "login": "cuishuang", - "name": "cui fliter", - "avatar_url": "https://avatars.githubusercontent.com/u/15921519?v=4", - "profile": "http://www.dashen.tech", - "contributions": [ - "content" - ] - }, - { - "login": "luskwater", - "name": "Ron Lusk", - "avatar_url": "https://avatars.githubusercontent.com/u/42529?v=4", - "profile": "https://github.com/luskwater", - "contributions": [ - "content" - ] - }, - { - "login": "liby", - "name": "Bryan Lee", - "avatar_url": "https://avatars.githubusercontent.com/u/38807139?v=4", - "profile": "http://liby.github.io/liby/", - "contributions": [ - "content" - ] - }, - { - "login": "nandajavarma", - "name": "Nandaja Varma", - "avatar_url": "https://avatars.githubusercontent.com/u/2624550?v=4", - "profile": "http://nandaja.space", - "contributions": [ - "doc" - ] - }, - { - "login": "merelymyself", - "name": "pwygab", - "avatar_url": "https://avatars.githubusercontent.com/u/88221256?v=4", - "profile": "https://github.com/merelymyself", - "contributions": [ - "code" - ] - }, - { - "login": "lucasgrvarela", - "name": "Lucas Grigolon Varela", - "avatar_url": "https://avatars.githubusercontent.com/u/37870368?v=4", - "profile": "http://linkedin.com/in/lucasgrvarela", - "contributions": [ - "content" - ] - }, - { - "login": "bufo24", - "name": "Bufo", - "avatar_url": "https://avatars.githubusercontent.com/u/32884105?v=4", - "profile": "https://github.com/bufo24", - "contributions": [ - "content" - ] - }, - { - "login": "jackos", - "name": "Jack Clayton", - "avatar_url": "https://avatars.githubusercontent.com/u/77730378?v=4", - "profile": "http://rustnote.com", - "contributions": [ - "code" - ] - }, - { - "login": "klkl0808", - "name": "Konstantin", - "avatar_url": "https://avatars.githubusercontent.com/u/24694249?v=4", - "profile": "https://github.com/klkl0808", - "contributions": [ - "content" - ] - }, - { - "login": "0pling", - "name": "0pling", - "avatar_url": "https://avatars.githubusercontent.com/u/104090344?v=4", - "profile": "https://github.com/0pling", - "contributions": [ - "content" - ] - }, - { - "login": "KatanaFluorescent", - "name": "KatanaFluorescent", - "avatar_url": "https://avatars.githubusercontent.com/u/60199077?v=4", - "profile": "https://github.com/KatanaFluorescent", - "contributions": [ - "code" - ] - }, - { - "login": "Drew-Morris", - "name": "Drew Morris", - "avatar_url": "https://avatars.githubusercontent.com/u/95818166?v=4", - "profile": "https://github.com/Drew-Morris", - "contributions": [ - "code" - ] - }, - { - "login": "camperdue42", - "name": "camperdue42", - "avatar_url": "https://avatars.githubusercontent.com/u/43047763?v=4", - "profile": "https://github.com/camperdue42", - "contributions": [ - "content" - ] - }, - { - "login": "YsuOS", - "name": "YsuOS", - "avatar_url": "https://avatars.githubusercontent.com/u/30138661?v=4", - "profile": "https://github.com/YsuOS", - "contributions": [ - "content" - ] - }, - { - "login": "icecream17", - "name": "Steven Nguyen", - "avatar_url": "https://avatars.githubusercontent.com/u/58114641?v=4", - "profile": "https://lichess.org/@/StevenEmily", - "contributions": [ - "content" - ] - }, - { - "login": "nacairns1", - "name": "nacairns1", - "avatar_url": "https://avatars.githubusercontent.com/u/94420090?v=4", - "profile": "https://noahcairns.dev", - "contributions": [ - "content" - ] - }, - { - "login": "pgjbz", - "name": "Paulo Gabriel Justino Bezerra", - "avatar_url": "https://avatars.githubusercontent.com/u/22059237?v=4", - "profile": "https://github.com/pgjbz", - "contributions": [ - "content" - ] - }, - { - "login": "jaystile", - "name": "Jason", - "avatar_url": "https://avatars.githubusercontent.com/u/46078028?v=4", - "profile": "https://github.com/jaystile", - "contributions": [ - "content" - ] - }, - { - "login": "exdx", - "name": "exdx", - "avatar_url": "https://avatars.githubusercontent.com/u/31546601?v=4", - "profile": "https://exdx.github.io", - "contributions": [ - "content" - ] - }, - { - "login": "Jzow", - "name": "James Zow", - "avatar_url": "https://avatars.githubusercontent.com/u/68860495?v=4", - "profile": "https://github.com/Jzow", - "contributions": [ - "content" - ] - }, - { - "login": "jayber", - "name": "James Bromley", - "avatar_url": "https://avatars.githubusercontent.com/u/2474334?v=4", - "profile": "https://jamesabromley.wordpress.com/", - "contributions": [ - "content" - ] - }, - { - "login": "swhiteCQC", - "name": "swhiteCQC", - "avatar_url": "https://avatars.githubusercontent.com/u/77438466?v=4", - "profile": "https://github.com/swhiteCQC", - "contributions": [ - "content" - ] - }, - { - "login": "neilpate", - "name": "Neil Pate", - "avatar_url": "https://avatars.githubusercontent.com/u/7802334?v=4", - "profile": "https://github.com/neilpate", - "contributions": [ - "content" - ] - }, - { - "login": "wojexe", - "name": "wojexe", - "avatar_url": "https://avatars.githubusercontent.com/u/21208490?v=4", - "profile": "https://wojexe.com", - "contributions": [ - "content" - ] - }, - { - "login": "Tostapunk", - "name": "Mattia Schiavon", - "avatar_url": "https://avatars.githubusercontent.com/u/25140297?v=4", - "profile": "https://github.com/Tostapunk", - "contributions": [ - "content" - ] - }, - { - "login": "PrettyWood", - "name": "Eric Jolibois", - "avatar_url": "https://avatars.githubusercontent.com/u/18406791?v=4", - "profile": "http://toucantoco.com", - "contributions": [ - "content" - ] - }, - { - "login": "EdwinChang24", - "name": "Edwin Chang", - "avatar_url": "https://avatars.githubusercontent.com/u/88263098?v=4", - "profile": "http://edwinchang.vercel.app", - "contributions": [ - "content" - ] - }, - { - "login": "saikatdas0790", - "name": "Saikat Das", - "avatar_url": "https://avatars.githubusercontent.com/u/7412443?v=4", - "profile": "https://saikat.dev/", - "contributions": [ - "content" - ] - }, - { - "login": "thatlittleboy", - "name": "Jeremy Goh", - "avatar_url": "https://avatars.githubusercontent.com/u/30731072?v=4", - "profile": "https://github.com/thatlittleboy", - "contributions": [ - "content" - ] - }, - { - "login": "Lioness100", - "name": "Lioness100", - "avatar_url": "https://avatars.githubusercontent.com/u/65814829?v=4", - "profile": "https://github.com/Lioness100", - "contributions": [ - "content" - ] - }, - { - "login": "tvkn", - "name": "Tristan Nicholls", - "avatar_url": "https://avatars.githubusercontent.com/u/79277926?v=4", - "profile": "https://github.com/tvkn", - "contributions": [ - "content" - ] - }, - { - "login": "clairew", - "name": "Claire", - "avatar_url": "https://avatars.githubusercontent.com/u/9344258?v=4", - "profile": "http://clairewang.net", - "contributions": [ - "content" - ] - }, - { - "login": "Mouwrice", - "name": "Maurice Van Wassenhove", - "avatar_url": "https://avatars.githubusercontent.com/u/56763273?v=4", - "profile": "https://github.com/Mouwrice", - "contributions": [ - "content" - ] - }, - { - "login": "johnmendel", - "name": "John Mendelewski", - "avatar_url": "https://avatars.githubusercontent.com/u/77524?v=4", - "profile": "http://jmthree.com", - "contributions": [ - "code" - ] - }, - { - "login": "brianfakhoury", - "name": "Brian Fakhoury", - "avatar_url": "https://avatars.githubusercontent.com/u/20828724?v=4", - "profile": "http://fakhoury.xyz", - "contributions": [ - "content" - ] - }, - { - "login": "markusboehme", - "name": "Markus Boehme", - "avatar_url": "https://avatars.githubusercontent.com/u/5074759?v=4", - "profile": "https://github.com/markusboehme", - "contributions": [ - "code" - ] - }, - { - "login": "nico-vromans", - "name": "Nico Vromans", - "avatar_url": "https://avatars.githubusercontent.com/u/48183857?v=4", - "profile": "https://github.com/nico-vromans", - "contributions": [ - "content" - ] - }, - { - "login": "vostok92", - "name": "vostok92", - "avatar_url": "https://avatars.githubusercontent.com/u/540339?v=4", - "profile": "https://github.com/vostok92", - "contributions": [ - "content" - ] - }, - { - "login": "magnusrodseth", - "name": "Magnus Rødseth", - "avatar_url": "https://avatars.githubusercontent.com/u/59113973?v=4", - "profile": "http://magnusrodseth.vercel.app", - "contributions": [ - "content" - ] - }, - { - "login": "rubiesonthesky", - "name": "rubiesonthesky", - "avatar_url": "https://avatars.githubusercontent.com/u/2591240?v=4", - "profile": "https://github.com/rubiesonthesky", - "contributions": [ - "content" - ] - }, - { - "login": "GabrielBianconi", - "name": "Gabriel Bianconi", - "avatar_url": "https://avatars.githubusercontent.com/u/1275491?v=4", - "profile": "http://www.gabrielbianconi.com/", - "contributions": [ - "content" - ] - }, - { - "login": "Kodylow", - "name": "Kody Low", - "avatar_url": "https://avatars.githubusercontent.com/u/74332828?v=4", - "profile": "https://github.com/Kodylow", - "contributions": [ - "content" - ] - }, - { - "login": "rzrymiak", - "name": "rzrymiak", - "avatar_url": "https://avatars.githubusercontent.com/u/106121613?v=4", - "profile": "https://github.com/rzrymiak", - "contributions": [ - "content" - ] - }, - { - "login": "miguelraz", - "name": "Miguel Raz Guzmán Macedo", - "avatar_url": "https://avatars.githubusercontent.com/u/13056181?v=4", - "profile": "https://github.com/miguelraz", - "contributions": [ - "content" - ] - }, - { - "login": "memark", - "name": "Magnus Markling", - "avatar_url": "https://avatars.githubusercontent.com/u/318504?v=4", - "profile": "https://github.com/memark", - "contributions": [ - "content" - ] - }, - { - "login": "gasparitiago", - "name": "Tiago De Gaspari", - "avatar_url": "https://avatars.githubusercontent.com/u/3237254?v=4", - "profile": "https://github.com/gasparitiago", - "contributions": [ - "content" - ] - }, - { - "login": "skaunov", - "name": "skaunov", - "avatar_url": "https://avatars.githubusercontent.com/u/65976143?v=4", - "profile": "https://github.com/skaunov", - "contributions": [ - "content" - ] - }, - { - "login": "cj81499", - "name": "Cal Jacobson", - "avatar_url": "https://avatars.githubusercontent.com/u/9152032?v=4", - "profile": "http://caljacobson.dev", - "contributions": [ - "content" - ] - }, - { - "login": "duchonic", - "name": "Duchoud Nicolas", - "avatar_url": "https://avatars.githubusercontent.com/u/34117620?v=4", - "profile": "https://github.com/duchonic", - "contributions": [ - "content" - ] - }, - { - "login": "gfaugere", - "name": "Gaëtan Faugère", - "avatar_url": "https://avatars.githubusercontent.com/u/11901979?v=4", - "profile": "https://github.com/gfaugere", - "contributions": [ - "tool" - ] - }, - { - "login": "bhbuehler", - "name": "bhbuehler", - "avatar_url": "https://avatars.githubusercontent.com/u/25541343?v=4", - "profile": "https://github.com/bhbuehler", - "contributions": [ - "content" - ] - }, - { - "login": "nyurik", - "name": "Yuri Astrakhan", - "avatar_url": "https://avatars.githubusercontent.com/u/1641515?v=4", - "profile": "https://github.com/nyurik", - "contributions": [ - "code" - ] - }, - { - "login": "azzamsa", - "name": "azzamsa", - "avatar_url": "https://avatars.githubusercontent.com/u/17734314?v=4", - "profile": "http://azzamsa.com", - "contributions": [ - "code" - ] - }, - { - "login": "mvanschellebeeck", - "name": "mvanschellebeeck", - "avatar_url": "https://avatars.githubusercontent.com/u/17671052?v=4", - "profile": "https://github.com/mvanschellebeeck", - "contributions": [ - "content" - ] - }, - { - "login": "aaarkid", - "name": "Arkid", - "avatar_url": "https://avatars.githubusercontent.com/u/39987510?v=4", - "profile": "https://github.com/aaarkid", - "contributions": [ - "content" - ] - }, - { - "login": "tfpk", - "name": "Tom Kunc", - "avatar_url": "https://avatars.githubusercontent.com/u/10906982?v=4", - "profile": "http://tfpk.dev", - "contributions": [ - "content" - ] - }, - { - "login": "mfurak", - "name": "Marek Furák", - "avatar_url": "https://avatars.githubusercontent.com/u/38523093?v=4", - "profile": "https://github.com/mfurak", - "contributions": [ - "content" - ] - }, - { - "login": "winterqt", - "name": "Winter", - "avatar_url": "https://avatars.githubusercontent.com/u/78392041?v=4", - "profile": "https://winter.cafe", - "contributions": [ - "code" - ] - }, - { - "login": "MoritzBoehme", - "name": "Moritz Böhme", - "avatar_url": "https://avatars.githubusercontent.com/u/42215704?v=4", - "profile": "https://moritzboeh.me", - "contributions": [ - "code" - ] - }, - { - "login": "craymel", - "name": "craymel", - "avatar_url": "https://avatars.githubusercontent.com/u/71062756?v=4", - "profile": "https://github.com/craymel", - "contributions": [ - "content" - ] - }, - { - "login": "tkburis", - "name": "TK Buristrakul", - "avatar_url": "https://avatars.githubusercontent.com/u/20501289?v=4", - "profile": "https://github.com/tkburis", - "contributions": [ - "content" - ] - }, - { - "login": "HerschelW", - "name": "Kent Worthington", - "avatar_url": "https://avatars.githubusercontent.com/u/17935816?v=4", - "profile": "https://github.com/HerschelW", - "contributions": [ - "content" - ] - }, - { - "login": "seporterfield", - "name": "seporterfield", - "avatar_url": "https://avatars.githubusercontent.com/u/107010978?v=4", - "profile": "https://github.com/seporterfield", - "contributions": [ - "content" - ] - }, - { - "login": "dbarrosop", - "name": "David Barroso", - "avatar_url": "https://avatars.githubusercontent.com/u/6246622?v=4", - "profile": "https://www.linkedin.com/in/dbarrosop", - "contributions": [ - "infra" - ] - }, - { - "login": "tklauser", - "name": "Tobias Klauser", - "avatar_url": "https://avatars.githubusercontent.com/u/539708?v=4", - "profile": "https://distanz.ch", - "contributions": [ - "code" - ] - }, - { - "login": "0xMySt1c", - "name": "0xMySt1c", - "avatar_url": "https://avatars.githubusercontent.com/u/101825630?v=4", - "profile": "https://github.com/0xMySt1c", - "contributions": [ - "tool" - ] - }, - { - "login": "AxolotlTears", - "name": "Ten", - "avatar_url": "https://avatars.githubusercontent.com/u/87157047?v=4", - "profile": "https://github.com/AxolotlTears", - "contributions": [ - "code" - ] - }, - { - "login": "h4x5p4c3", - "name": "jones martin", - "avatar_url": "https://avatars.githubusercontent.com/u/66133688?v=4", - "profile": "http://h4x5p4c3.xyz", - "contributions": [ - "content" - ] - }, - { - "login": "cloppingemu", - "name": "cloppingemu", - "avatar_url": "https://avatars.githubusercontent.com/u/12227963?v=4", - "profile": "https://github.com/cloppingemu", - "contributions": [ - "code" - ] - }, - { - "login": "kevwan", - "name": "Kevin Wan", - "avatar_url": "https://avatars.githubusercontent.com/u/1918356?v=4", - "profile": "http://github.com/zeromicro/go-zero", - "contributions": [ - "content" - ] - }, - { - "login": "wjwrh", - "name": "Ruby", - "avatar_url": "https://avatars.githubusercontent.com/u/43495006?v=4", - "profile": "http://kurowasaruby.cn", - "contributions": [ - "code" - ] - }, - { - "login": "alexandergill", - "name": "Alexander Gill", - "avatar_url": "https://avatars.githubusercontent.com/u/7033716?v=4", - "profile": "https://github.com/alexandergill", - "contributions": [ - "code" - ] - }, - { - "login": "kawaiiPlat", - "name": "Jarrod Sanders", - "avatar_url": "https://avatars.githubusercontent.com/u/50600614?v=4", - "profile": "https://www.linkedin.com/in/jarrod-sanders/", - "contributions": [ - "content" - ] - }, - { - "login": "platformer", - "name": "Andrew Sen", - "avatar_url": "https://avatars.githubusercontent.com/u/40146328?v=4", - "profile": "https://github.com/platformer", - "contributions": [ - "content" - ] - }, - { - "login": "grzegorz-zur", - "name": "Grzegorz Żur", - "avatar_url": "https://avatars.githubusercontent.com/u/5297583?v=4", - "profile": "https://grzegorz-zur.com/", - "contributions": [ - "content" - ] - }, - { - "login": "black-puppydog", - "name": "Daan Wynen", - "avatar_url": "https://avatars.githubusercontent.com/u/189241?v=4", - "profile": "https://github.com/black-puppydog", - "contributions": [ - "content" - ] - }, - { - "login": "Anush008", - "name": "Anush", - "avatar_url": "https://avatars.githubusercontent.com/u/46051506?v=4", - "profile": "https://github.com/Anush008", - "contributions": [ - "doc" - ] - }, - { - "login": "shgew", - "name": "Gleb Shevchenko", - "avatar_url": "https://avatars.githubusercontent.com/u/5584672?v=4", - "profile": "https://github.com/shgew", - "contributions": [ - "content" - ] - }, - { - "login": "mdmundo", - "name": "Edmundo Paulino", - "avatar_url": "https://avatars.githubusercontent.com/u/60408300?v=4", - "profile": "https://github.com/mdmundo", - "contributions": [ - "infra" - ] - }, - { - "login": "eroullit", - "name": "Emmanuel Roullit", - "avatar_url": "https://avatars.githubusercontent.com/u/301795?v=4", - "profile": "https://github.com/eroullit", - "contributions": [ - "infra" - ] - }, - { - "login": "nidhalmessaoudi", - "name": "Nidhal Messaoudi", - "avatar_url": "https://avatars.githubusercontent.com/u/63377412?v=4", - "profile": "https://nidhalmessaoudi.herokuapp.com", - "contributions": [ - "code" - ] - }, - { - "login": "MahdiBM", - "name": "Mahdi Bahrami", - "avatar_url": "https://avatars.githubusercontent.com/u/54685446?v=4", - "profile": "https://github.com/MahdiBM", - "contributions": [ - "tool" - ] - }, - { - "login": "Nagidal", - "name": "Nagidal", - "avatar_url": "https://avatars.githubusercontent.com/u/7075397?v=4", - "profile": "https://github.com/Nagidal", - "contributions": [ - "content" - ] - }, - { - "login": "adamhb123", - "name": "Adam Brewer", - "avatar_url": "https://avatars.githubusercontent.com/u/25161597?v=4", - "profile": "https://adabrew.com", - "contributions": [ - "content" - ] - }, - { - "login": "eugkhp", - "name": "Eugene", - "avatar_url": "https://avatars.githubusercontent.com/u/25910599?v=4", - "profile": "https://github.com/eugkhp", - "contributions": [ - "tool" - ] - }, - { - "login": "navicore", - "name": "Ed Sweeney", - "avatar_url": "https://avatars.githubusercontent.com/u/110999?v=4", - "profile": "https://social.linux.pizza/@navicore", - "contributions": [ - "content" - ] - }, - { - "login": "javihernant", - "name": "javihernant", - "avatar_url": "https://avatars.githubusercontent.com/u/73640929?v=4", - "profile": "https://github.com/javihernant", - "contributions": [ - "content" - ] - }, - { - "login": "VegardMatthey", - "name": "Vegard", - "avatar_url": "https://avatars.githubusercontent.com/u/59250656?v=4", - "profile": "https://github.com/VegardMatthey", - "contributions": [ - "content" - ] - }, - { - "login": "ryanwhitehouse", - "name": "Ryan Whitehouse", - "avatar_url": "https://avatars.githubusercontent.com/u/13400784?v=4", - "profile": "https://github.com/ryanwhitehouse", - "contributions": [ - "content" - ] - }, - { - "login": "guoard", - "name": "Ali Afsharzadeh", - "avatar_url": "https://avatars.githubusercontent.com/u/65511355?v=4", - "profile": "https://github.com/guoard", - "contributions": [ - "content" - ] - }, - { - "login": "keogami", - "name": "Keogami", - "avatar_url": "https://avatars.githubusercontent.com/u/41939011?v=4", - "profile": "http://keogami.ml", - "contributions": [ - "doc" - ] - }, - { - "login": "ahresse", - "name": "Alexandre Esse", - "avatar_url": "https://avatars.githubusercontent.com/u/28402488?v=4", - "profile": "https://github.com/ahresse", - "contributions": [ - "content" - ] - }, - { - "login": "sagarvora", - "name": "Sagar Vora", - "avatar_url": "https://avatars.githubusercontent.com/u/16315650?v=4", - "profile": "https://resilient.tech", - "contributions": [ - "content" - ] - }, - { - "login": "poneciak57", - "name": "Kacper Poneta", - "avatar_url": "https://avatars.githubusercontent.com/u/94321164?v=4", - "profile": "https://github.com/poneciak57", - "contributions": [ - "content" - ] - }, - { - "login": "ktheory", - "name": "Aaron Suggs", - "avatar_url": "https://avatars.githubusercontent.com/u/975?v=4", - "profile": "https://ktheory.com/", - "contributions": [ - "content" - ] - }, - { - "login": "alexwh", - "name": "Alex", - "avatar_url": "https://avatars.githubusercontent.com/u/1723612?v=4", - "profile": "https://github.com/alexwh", - "contributions": [ - "content" - ] - }, - { - "login": "stornquist", - "name": "Sebastian Törnquist", - "avatar_url": "https://avatars.githubusercontent.com/u/42915664?v=4", - "profile": "https://github.com/stornquist", - "contributions": [ - "content" - ] - }, - { - "login": "smlavine", - "name": "Sebastian LaVine", - "avatar_url": "https://avatars.githubusercontent.com/u/33563640?v=4", - "profile": "http://smlavine.com", - "contributions": [ - "code" - ] - }, - { - "login": "akgerber", - "name": "Alan Gerber", - "avatar_url": "https://avatars.githubusercontent.com/u/201313?v=4", - "profile": "http://www.alangerber.us", - "contributions": [ - "content" - ] - }, - { - "login": "esotuvaka", - "name": "Eric", - "avatar_url": "https://avatars.githubusercontent.com/u/104941850?v=4", - "profile": "http://esotuvaka.github.io", - "contributions": [ - "content" - ] - }, - { - "login": "az0977776", - "name": "Aaron Wang", - "avatar_url": "https://avatars.githubusercontent.com/u/9172038?v=4", - "profile": "https://github.com/az0977776", - "contributions": [ - "content" - ] - }, - { - "login": "nmay231", - "name": "Noah", - "avatar_url": "https://avatars.githubusercontent.com/u/35386821?v=4", - "profile": "https://github.com/nmay231", - "contributions": [ - "content" - ] - }, - { - "login": "rb5014", - "name": "rb5014", - "avatar_url": "https://avatars.githubusercontent.com/u/105397317?v=4", - "profile": "https://github.com/rb5014", - "contributions": [ - "content" - ] - }, - { - "login": "deedy5", - "name": "deedy5", - "avatar_url": "https://avatars.githubusercontent.com/u/65482418?v=4", - "profile": "https://github.com/deedy5", - "contributions": [ - "content" - ] - }, - { - "login": "lionel-rowe", - "name": "lionel-rowe", - "avatar_url": "https://avatars.githubusercontent.com/u/26078826?v=4", - "profile": "https://github.com/lionel-rowe", - "contributions": [ - "content" - ] - }, - { - "login": "Ben2917", - "name": "Ben", - "avatar_url": "https://avatars.githubusercontent.com/u/10279994?v=4", - "profile": "https://github.com/Ben2917", - "contributions": [ - "content" - ] - }, - { - "login": "b1ue64", - "name": "b1ue64", - "avatar_url": "https://avatars.githubusercontent.com/u/77976308?v=4", - "profile": "https://github.com/b1ue64", - "contributions": [ - "content" - ] - }, - { - "login": "lazywalker", - "name": "lazywalker", - "avatar_url": "https://avatars.githubusercontent.com/u/53956?v=4", - "profile": "https://github.com/lazywalker", - "contributions": [ - "content" - ] - }, - { - "login": "proofconstruction", - "name": "proofconstruction", - "avatar_url": "https://avatars.githubusercontent.com/u/74747193?v=4", - "profile": "https://github.com/proofconstruction", - "contributions": [ - "infra" - ] - }, - { - "login": "IVIURRAY", - "name": "IVIURRAY", - "avatar_url": "https://avatars.githubusercontent.com/u/16007179?v=4", - "profile": "https://www.youtube.com/channel/UCQCjA6qUutAtWqkCA4Z36CQ", - "contributions": [ - "content" - ] - }, - { - "login": "b-apperlo", - "name": "Bert Apperlo", - "avatar_url": "https://avatars.githubusercontent.com/u/91734527?v=4", - "profile": "https://github.com/b-apperlo", - "contributions": [ - "content" - ] - }, - { - "login": "FWDekker", - "name": "Florine W. Dekker", - "avatar_url": "https://avatars.githubusercontent.com/u/13442533?v=4", - "profile": "https://fwdekker.com/", - "contributions": [ - "content" - ] - }, - { - "login": "luhem7", - "name": "Mehul Gangavelli", - "avatar_url": "https://avatars.githubusercontent.com/u/4008215?v=4", - "profile": "https://github.com/luhem7", - "contributions": [ - "content" - ] - }, - { - "login": "Frosthage", - "name": "Mikael Frosthage", - "avatar_url": "https://avatars.githubusercontent.com/u/14823314?v=4", - "profile": "https://github.com/Frosthage", - "contributions": [ - "content" - ] - }, - { - "login": "robertefry", - "name": "Robert Fry", - "avatar_url": "https://avatars.githubusercontent.com/u/43712054?v=4", - "profile": "https://robertfry.xyz", - "contributions": [ - "content" - ] - }, - { - "login": "tajo48", - "name": "tajo48", - "avatar_url": "https://avatars.githubusercontent.com/u/55502906?v=4", - "profile": "https://github.com/tajo48", - "contributions": [ - "content" - ] - }, - { - "login": "novanish", - "name": "Anish", - "avatar_url": "https://avatars.githubusercontent.com/u/98446102?v=4", - "profile": "https://anishchhetri.com.np", - "contributions": [ - "content" - ] - }, - { - "login": "vnprc", - "name": "vnprc", - "avatar_url": "https://avatars.githubusercontent.com/u/9425366?v=4", - "profile": "https://github.com/vnprc", - "contributions": [ - "content" - ] - }, - { - "login": "jrcarl624", - "name": "Joshua Carlson", - "avatar_url": "https://avatars.githubusercontent.com/u/61999256?v=4", - "profile": "http://androecia.net", - "contributions": [ - "content" - ] - }, - { - "login": "johnDeSilencio", - "name": "Nicholas R. Smith", - "avatar_url": "https://avatars.githubusercontent.com/u/20136554?v=4", - "profile": "https://johndesilencio.me", - "contributions": [ - "code" - ] - }, - { - "login": "alexfertel", - "name": "Alexander González", - "avatar_url": "https://avatars.githubusercontent.com/u/22298999?v=4", - "profile": "https://alexfertel.me", - "contributions": [ - "content" - ] - }, - { - "login": "softarn", - "name": "Marcus Höjvall", - "avatar_url": "https://avatars.githubusercontent.com/u/517619?v=4", - "profile": "https://github.com/softarn", - "contributions": [ - "content" - ] - }, - { - "login": "barlevalon", - "name": "Alon Hearter", - "avatar_url": "https://avatars.githubusercontent.com/u/3397911?v=4", - "profile": "https://github.com/barlevalon", - "contributions": [ - "content" - ] - }, - { - "login": "shirts", - "name": "shirts", - "avatar_url": "https://avatars.githubusercontent.com/u/4952151?v=4", - "profile": "https://github.com/shirts", - "contributions": [ - "content" - ] - }, - { - "login": "eLVas", - "name": "Ivan Vasiunyk", - "avatar_url": "https://avatars.githubusercontent.com/u/6797156?v=4", - "profile": "https://github.com/eLVas", - "contributions": [ - "content" - ] - }, - { - "login": "mo8it", - "name": "Mo", - "avatar_url": "https://avatars.githubusercontent.com/u/76752051?v=4", - "profile": "https://mo8it.com", - "contributions": [ - "code" - ] - }, - { - "login": "x10an14", - "name": "x10an14", - "avatar_url": "https://avatars.githubusercontent.com/u/710608?v=4", - "profile": "https://github.com/x10an14", - "contributions": [ - "infra" - ] - }, - { - "login": "gabay", - "name": "Roi Gabay", - "avatar_url": "https://avatars.githubusercontent.com/u/5773610?v=4", - "profile": "https://github.com/gabay", - "contributions": [ - "content" - ] - }, - { - "login": "mkovaxx", - "name": "Máté Kovács", - "avatar_url": "https://avatars.githubusercontent.com/u/481354?v=4", - "profile": "https://github.com/mkovaxx", - "contributions": [ - "content" - ] - }, - { - "login": "szabgab", - "name": "Gábor Szabó", - "avatar_url": "https://avatars.githubusercontent.com/u/48833?v=4", - "profile": "https://szabgab.com/", - "contributions": [ - "content" - ] - }, - { - "login": "yamila-moreno", - "name": "Yamila Moreno", - "avatar_url": "https://avatars.githubusercontent.com/u/3340793?v=4", - "profile": "https://moduslaborandi.net", - "contributions": [ - "content" - ] - }, - { - "login": "willhack", - "name": "Will Hack", - "avatar_url": "https://avatars.githubusercontent.com/u/18036720?v=4", - "profile": "https://github.com/willhack", - "contributions": [ - "content" - ] - }, - { - "login": "bean5", - "name": "Michael", - "avatar_url": "https://avatars.githubusercontent.com/u/2052646?v=4", - "profile": "http://cancompute.tech", - "contributions": [ - "content" - ] - }, - { - "login": "pksadiq", - "name": "Mohammed Sadiq", - "avatar_url": "https://avatars.githubusercontent.com/u/1289514?v=4", - "profile": "https://www.sadiqpk.org", - "contributions": [ - "content" - ] - }, - { - "login": "Jak-Ch-ll", - "name": "Jakob", - "avatar_url": "https://avatars.githubusercontent.com/u/56225668?v=4", - "profile": "https://github.com/Jak-Ch-ll", - "contributions": [ - "content" - ] - }, - { - "login": "ob", - "name": "Oscar Bonilla", - "avatar_url": "https://avatars.githubusercontent.com/u/4950?v=4", - "profile": "http://oscarbonilla.com", - "contributions": [ - "content" - ] - }, - { - "login": "husjon", - "name": "Jon Erling Hustadnes", - "avatar_url": "https://avatars.githubusercontent.com/u/554229?v=4", - "profile": "https://github.com/husjon", - "contributions": [ - "content" - ] - }, - { - "login": "CobaltCause", - "name": "Charles Hall", - "avatar_url": "https://avatars.githubusercontent.com/u/7003738?v=4", - "profile": "https://github.com/CobaltCause", - "contributions": [ - "infra" - ] - }, - { - "login": "krmpotic", - "name": "Luka Krmpotić", - "avatar_url": "https://avatars.githubusercontent.com/u/10350645?v=4", - "profile": "https://github.com/krmpotic", - "contributions": [ - "content" - ] - }, - { - "login": "jurglic", - "name": "Jurglic", - "avatar_url": "https://avatars.githubusercontent.com/u/112600?v=4", - "profile": "https://github.com/jurglic", - "contributions": [ - "content" - ] - }, - { - "login": "OfirLauber", - "name": "Ofir Lauber", - "avatar_url": "https://avatars.githubusercontent.com/u/5631030?v=4", - "profile": "https://github.com/OfirLauber", - "contributions": [ - "content" - ] - }, - { - "login": "offbyone", - "name": "Chris Rose", - "avatar_url": "https://avatars.githubusercontent.com/u/181693?v=4", - "profile": "https://github.com/offbyone", - "contributions": [ - "infra" - ] - }, - { - "login": "dieterplex", - "name": "d1t2", - "avatar_url": "https://avatars.githubusercontent.com/u/507502?v=4", - "profile": "https://github.com/dieterplex", - "contributions": [ - "infra" - ] - }, - { - "login": "docwilco", - "name": "docwilco", - "avatar_url": "https://avatars.githubusercontent.com/u/66911096?v=4", - "profile": "https://github.com/docwilco", - "contributions": [ - "code" - ] - }, - { - "login": "matthewjnield", - "name": "Matt Nield", - "avatar_url": "https://avatars.githubusercontent.com/u/64328730?v=4", - "profile": "https://www.linkedin.com/in/matthew-nield1/", - "contributions": [ - "content" - ] - }, - { - "login": "TheBearodactyl", - "name": "The Bearodactyl", - "avatar_url": "https://avatars.githubusercontent.com/u/114454115?v=4", - "profile": "https://github.com/TheBearodactyl", - "contributions": [ - "code" - ] - }, - { - "login": "markgreene74", - "name": "markgreene74", - "avatar_url": "https://avatars.githubusercontent.com/u/18945890?v=4", - "profile": "https://github.com/markgreene74", - "contributions": [ - "code" - ] - }, - { - "login": "VeeDeltaVee", - "name": "Versha Dhankar", - "avatar_url": "https://avatars.githubusercontent.com/u/45564258?v=4", - "profile": "https://github.com/VeeDeltaVee", - "contributions": [ - "doc" - ] - }, - { - "login": "0atman", - "name": "Tristram Oaten", - "avatar_url": "https://avatars.githubusercontent.com/u/114097?v=4", - "profile": "http://0atman.com", - "contributions": [ - "content" - ] - }, - { - "login": "danieltinazzi", - "name": "Daniel Tinazzi", - "avatar_url": "https://avatars.githubusercontent.com/u/11833533?v=4", - "profile": "https://github.com/danieltinazzi", - "contributions": [ - "content" - ] - }, - { - "login": "raymon-roos", - "name": "Raymon Roos", - "avatar_url": "https://avatars.githubusercontent.com/u/38888470?v=4", - "profile": "https://github.com/raymon-roos", - "contributions": [ - "content" - ] - }, - { - "login": "matthri", - "name": "Matthias", - "avatar_url": "https://avatars.githubusercontent.com/u/67913999?v=4", - "profile": "https://github.com/matthri", - "contributions": [ - "code" - ] - }, - { - "login": "neuschaefer", - "name": "J. Neuschäfer", - "avatar_url": "https://avatars.githubusercontent.com/u/1021512?v=4", - "profile": "https://github.com/neuschaefer", - "contributions": [ - "code" - ] - }, - { - "login": "bastianpedersen", - "name": "Bastian Pedersen", - "avatar_url": "https://avatars.githubusercontent.com/u/58905488?v=4", - "profile": "https://scooterhacking.org", - "contributions": [ - "content" - ] - }, - { - "login": "gerases", - "name": "gerases", - "avatar_url": "https://avatars.githubusercontent.com/u/8953623?v=4", - "profile": "https://github.com/gerases", - "contributions": [ - "content" - ] - }, - { - "login": "AnonimAnonim2245", - "name": "Luca Plian", - "avatar_url": "https://avatars.githubusercontent.com/u/98339220?v=4", - "profile": "https://github.com/AnonimAnonim2245", - "contributions": [ - "code" - ] - }, - { - "login": "reifenrath-dev", - "name": "René Reifenrath", - "avatar_url": "https://avatars.githubusercontent.com/u/18126097?v=4", - "profile": "https://reifenrath.dev/", - "contributions": [ - "content" - ] - }, - { - "login": "peterneave", - "name": "Peter Neave", - "avatar_url": "https://avatars.githubusercontent.com/u/7982708?v=4", - "profile": "https://github.com/peterneave", - "contributions": [ - "infra" - ] - }, - { - "login": "JanB1", - "name": "Jan", - "avatar_url": "https://avatars.githubusercontent.com/u/5552248?v=4", - "profile": "http://www.janb1.com", - "contributions": [ - "content" - ] - }, - { - "login": "kylev", - "name": "Kyle VanderBeek", - "avatar_url": "https://avatars.githubusercontent.com/u/46888?v=4", - "profile": "http://www.kylev.com/", - "contributions": [ - "infra" - ] - }, - { - "login": "pavedroad", - "name": "pavedroad", - "avatar_url": "https://avatars.githubusercontent.com/u/138004431?v=4", - "profile": "https://github.com/pavedroad", - "contributions": [ - "content" - ] - }, - { - "login": "hyphena", - "name": "luna", - "avatar_url": "https://avatars.githubusercontent.com/u/26529488?v=4", - "profile": "https://github.com/hyphena", - "contributions": [ - "content" - ] - }, - { - "login": "evanmiller2112", - "name": "Evan Miller", - "avatar_url": "https://avatars.githubusercontent.com/u/28488957?v=4", - "profile": "https://github.com/evanmiller2112", - "contributions": [ - "content" - ] - }, - { - "login": "luvchurchill", - "name": "luvchurchill", - "avatar_url": "https://avatars.githubusercontent.com/u/46406654?v=4", - "profile": "https://github.com/luvchurchill", - "contributions": [ - "code" - ] - }, - { - "login": "LeverImmy", - "name": "Ze-en Xiong", - "avatar_url": "https://avatars.githubusercontent.com/u/47663913?v=4", - "profile": "https://leverimmy.top/", - "contributions": [ - "content" - ] - }, - { - "login": "parnavh", - "name": "Parnav Harinathan", - "avatar_url": "https://avatars.githubusercontent.com/u/45985534?v=4", - "profile": "https://github.com/parnavh", - "contributions": [ - "content" - ] - }, - { - "login": "0Ahmed-0", - "name": "0Ahmed-0", - "avatar_url": "https://avatars.githubusercontent.com/u/111569638?v=4", - "profile": "https://github.com/0Ahmed-0", - "contributions": [ - "content" - ] - }, - { - "login": "guizo792", - "name": "guizo792", - "avatar_url": "https://avatars.githubusercontent.com/u/95940388?v=4", - "profile": "https://github.com/guizo792", - "contributions": [ - "content" - ] - }, - { - "login": "kazu728", - "name": "Kazuki Matsuo", - "avatar_url": "https://avatars.githubusercontent.com/u/34614358?v=4", - "profile": "https://github.com/kazu728", - "contributions": [ - "code" - ] - }, - { - "login": "paul-leydier", - "name": "Paul Leydier", - "avatar_url": "https://avatars.githubusercontent.com/u/75126792?v=4", - "profile": "https://github.com/paul-leydier", - "contributions": [ - "doc" - ] - }, - { - "login": "wznmickey", - "name": "wznmickey", - "avatar_url": "https://avatars.githubusercontent.com/u/44784663?v=4", - "profile": "http://wznmickey.com", - "contributions": [ - "doc" - ] - }, - { - "login": "NicolasRoelandt", - "name": "NicolasRoelandt", - "avatar_url": "https://avatars.githubusercontent.com/u/8594193?v=4", - "profile": "https://github.com/NicolasRoelandt", - "contributions": [ - "doc" - ] - }, - { - "login": "jbouganim-parallel", - "name": "Josh Bouganim", - "avatar_url": "https://avatars.githubusercontent.com/u/150748285?v=4", - "profile": "https://github.com/jbouganim-parallel", - "contributions": [ - "code" - ] - }, - { - "login": "loshz", - "name": "Dan", - "avatar_url": "https://avatars.githubusercontent.com/u/3449337?v=4", - "profile": "https://loshz.com", - "contributions": [ - "code" - ] - } - ], - "contributorsPerLine": 8, - "projectName": "rustlings", - "projectOwner": "rust-lang", - "repoType": "github", - "repoHost": "https://github.com", - "skipCi": true, - "commitConvention": "angular", - "commitType": "docs" -} diff --git a/AUTHORS.md b/AUTHORS.md deleted file mode 100644 index d82a5cab..00000000 --- a/AUTHORS.md +++ /dev/null @@ -1,397 +0,0 @@ -## Authors - -This file lists the people that have contributed to this project. - -Excluded from this list are @carols10cents and @diannasoreil, the principal -authors. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Carol (Nichols || Goulding)
Carol (Nichols || Goulding)

💻 🖋
QuietMisdreavus
QuietMisdreavus

💻 🖋
Robert M Lugg
Robert M Lugg

🖋
Hynek Schlawack
Hynek Schlawack

💻
Katharina Fey
Katharina Fey

💻
lukabavdaz
lukabavdaz

💻 🖋
Erik Vesteraas
Erik Vesteraas

💻
delet0r
delet0r

💻
Shaun Bennett
Shaun Bennett

💻
Andrew Bagshaw
Andrew Bagshaw

💻
Kyle Isom
Kyle Isom

💻
Colin Pitrat
Colin Pitrat

💻
Zac Anger
Zac Anger

💻
Matthias Geier
Matthias Geier

💻
Chris Pearce
Chris Pearce

💻
Yvan Sraka
Yvan Sraka

💻
Denys Smirnov
Denys Smirnov

💻
eddyp
eddyp

💻
Brian Kung
Brian Kung

💻 🖋
Russell
Russell

💻
Dan Wilhelm
Dan Wilhelm

📖
Jesse
Jesse

💻 🖋
Fredrik Jambrén
Fredrik Jambrén

💻
Pete McFarlane
Pete McFarlane

🖋
nkanderson
nkanderson

💻 🖋
Ajax M
Ajax M

📖
Dylan Nugent
Dylan Nugent

🖋
vyaslav
vyaslav

💻 🖋
George
George

💻
Thomas Holloway
Thomas Holloway

💻 🖋
Jubilee
Jubilee

💻
WofWca
WofWca

💻
Roberto Vidal
Roberto Vidal

💻 📖 🤔 🚧
Jens
Jens

📖
Rahat Ahmed
Rahat Ahmed

📖
Abdou Seck
Abdou Seck

💻 🖋 👀
Katie
Katie

💻
Socrates
Socrates

📖
gnodarse
gnodarse

🖋
Harrison Metzger
Harrison Metzger

💻
Torben Jonas
Torben Jonas

💻 🖋
Paul Bissex
Paul Bissex

📖
Steven Mann
Steven Mann

💻 🖋
Mario Reder
Mario Reder

💻 🖋
skim
skim

💻
Sanjay K
Sanjay K

💻 🖋
Rohan Jain
Rohan Jain

💻
Said Aspen
Said Aspen

💻 🖋
Ufuk Celebi
Ufuk Celebi

💻
lebedevsergey
lebedevsergey

📖
Aleksei Trifonov
Aleksei Trifonov

🖋
Darren Meehan
Darren Meehan

🖋
Jihchi Lee
Jihchi Lee

🖋
Christofer Bertonha
Christofer Bertonha

🖋
Vivek Bharath Akupatni
Vivek Bharath Akupatni

💻 ⚠️
Dídac Sementé Fernández
Dídac Sementé Fernández

💻 🖋
Rob Story
Rob Story

💻
Siobhan Jacobson
Siobhan Jacobson

💻
Evan Carroll
Evan Carroll

🖋
Jawaad Mahmood
Jawaad Mahmood

🖋
Gaurang Tandon
Gaurang Tandon

🖋
Stefan Kupresak
Stefan Kupresak

🖋
Greg Leonard
Greg Leonard

🖋
Ryan McQuen
Ryan McQuen

💻
Annika
Annika

👀
Axel Viala
Axel Viala

💻
Mohammed Sazid Al Rashid
Mohammed Sazid Al Rashid

🖋 💻
Caleb Webber
Caleb Webber

🚧
Peter N
Peter N

🚧
seancad
seancad

🚧
Will Hayworth
Will Hayworth

🖋
Christian Zeller
Christian Zeller

🖋
Jean-Francois Chevrette
Jean-Francois Chevrette

🖋 💻
John Baber-Lucero
John Baber-Lucero

🖋
Tal
Tal

🖋
apogeeoak
apogeeoak

🖋 💻
Larry Garfield
Larry Garfield

🖋
circumspect
circumspect

🖋
Cyrus Wyett
Cyrus Wyett

🖋
cadolphs
cadolphs

💻
Pascal H.
Pascal H.

🖋
Rod Elias
Rod Elias

🖋
Matt Lebl
Matt Lebl

💻
Ignacio Le Fluk
Ignacio Le Fluk

🖋
Taylor Yu
Taylor Yu

💻 🖋
Patrick Hintermayer
Patrick Hintermayer

💻
Pete Pavlovski
Pete Pavlovski

🖋
k12ish
k12ish

🖋
Shao Yang Hong
Shao Yang Hong

🖋
Brandon Macer
Brandon Macer

🖋
Stoian Dan
Stoian Dan

🖋
Pi Delport
Pi Delport

🖋
Sateesh
Sateesh

💻 🖋
ZC
ZC

🖋
hyperparabolic
hyperparabolic

💻
arlecchino
arlecchino

📖
Richthofen
Richthofen

💻
Ivan Nerazumov
Ivan Nerazumov

📖
lauralindzey
lauralindzey

📖
Rakshit Sinha
Rakshit Sinha

🖋
Damian
Damian

🖋
Ben Armstead
Ben Armstead

💻
anuk909
anuk909

🖋 💻
granddaifuku
granddaifuku

🖋
Weilet
Weilet

🖋
LIU JIE
LIU JIE

🖋
Antoine Büsch
Antoine Büsch

💻
frogtd
frogtd

🖋
Zhenghao Lu
Zhenghao Lu

🖋
Fredrik Enestad
Fredrik Enestad

🖋
xuesong
xuesong

🖋
Michael Walsh
Michael Walsh

💻
alirezaghey
alirezaghey

🖋
Franklin van Nes
Franklin van Nes

💻
nekonako
nekonako

💻
ZX
ZX

🖋
Yang Wen
Yang Wen

🖋
Brandon High
Brandon High

📖
x-hgg-x
x-hgg-x

💻
Kisaragi
Kisaragi

📖
Lucas Aries
Lucas Aries

🖋
ragreenburg
ragreenburg

🖋
stevenfukase
stevenfukase

🖋
J-S-Kim
J-S-Kim

🖋
Fointard
Fointard

🖋
Ryan Lowe
Ryan Lowe

💻
cui fliter
cui fliter

🖋
Ron Lusk
Ron Lusk

🖋
Bryan Lee
Bryan Lee

🖋
Nandaja Varma
Nandaja Varma

📖
pwygab
pwygab

💻
Lucas Grigolon Varela
Lucas Grigolon Varela

🖋
Bufo
Bufo

🖋
Jack Clayton
Jack Clayton

💻
Konstantin
Konstantin

🖋
0pling
0pling

🖋
KatanaFluorescent
KatanaFluorescent

💻
Drew Morris
Drew Morris

💻
camperdue42
camperdue42

🖋
YsuOS
YsuOS

🖋
Steven Nguyen
Steven Nguyen

🖋
nacairns1
nacairns1

🖋
Paulo Gabriel Justino Bezerra
Paulo Gabriel Justino Bezerra

🖋
Jason
Jason

🖋
exdx
exdx

🖋
James Zow
James Zow

🖋
James Bromley
James Bromley

🖋
swhiteCQC
swhiteCQC

🖋
Neil Pate
Neil Pate

🖋
wojexe
wojexe

🖋
Mattia Schiavon
Mattia Schiavon

🖋
Eric Jolibois
Eric Jolibois

🖋
Edwin Chang
Edwin Chang

🖋
Saikat Das
Saikat Das

🖋
Jeremy Goh
Jeremy Goh

🖋
Lioness100
Lioness100

🖋
Tristan Nicholls
Tristan Nicholls

🖋
Claire
Claire

🖋
Maurice Van Wassenhove
Maurice Van Wassenhove

🖋
John Mendelewski
John Mendelewski

💻
Brian Fakhoury
Brian Fakhoury

🖋
Markus Boehme
Markus Boehme

💻
Nico Vromans
Nico Vromans

🖋
vostok92
vostok92

🖋
Magnus Rødseth
Magnus Rødseth

🖋
rubiesonthesky
rubiesonthesky

🖋
Gabriel Bianconi
Gabriel Bianconi

🖋
Kody Low
Kody Low

🖋
rzrymiak
rzrymiak

🖋
Miguel Raz Guzmán Macedo
Miguel Raz Guzmán Macedo

🖋
Magnus Markling
Magnus Markling

🖋
Tiago De Gaspari
Tiago De Gaspari

🖋
skaunov
skaunov

🖋
Cal Jacobson
Cal Jacobson

🖋
Duchoud Nicolas
Duchoud Nicolas

🖋
Gaëtan Faugère
Gaëtan Faugère

🔧
bhbuehler
bhbuehler

🖋
Yuri Astrakhan
Yuri Astrakhan

💻
azzamsa
azzamsa

💻
mvanschellebeeck
mvanschellebeeck

🖋
Arkid
Arkid

🖋
Tom Kunc
Tom Kunc

🖋
Marek Furák
Marek Furák

🖋
Winter
Winter

💻
Moritz Böhme
Moritz Böhme

💻
craymel
craymel

🖋
TK Buristrakul
TK Buristrakul

🖋
Kent Worthington
Kent Worthington

🖋
seporterfield
seporterfield

🖋
David Barroso
David Barroso

🚇
Tobias Klauser
Tobias Klauser

💻
0xMySt1c
0xMySt1c

🔧
Ten
Ten

💻
jones martin
jones martin

🖋
cloppingemu
cloppingemu

💻
Kevin Wan
Kevin Wan

🖋
Ruby
Ruby

💻
Alexander Gill
Alexander Gill

💻
Jarrod Sanders
Jarrod Sanders

🖋
Andrew Sen
Andrew Sen

🖋
Grzegorz Żur
Grzegorz Żur

🖋
Daan Wynen
Daan Wynen

🖋
Anush
Anush

📖
Gleb Shevchenko
Gleb Shevchenko

🖋
Edmundo Paulino
Edmundo Paulino

🚇
Emmanuel Roullit
Emmanuel Roullit

🚇
Nidhal Messaoudi
Nidhal Messaoudi

💻
Mahdi Bahrami
Mahdi Bahrami

🔧
Nagidal
Nagidal

🖋
Adam Brewer
Adam Brewer

🖋
Eugene
Eugene

🔧
Ed Sweeney
Ed Sweeney

🖋
javihernant
javihernant

🖋
Vegard
Vegard

🖋
Ryan Whitehouse
Ryan Whitehouse

🖋
Ali Afsharzadeh
Ali Afsharzadeh

🖋
Keogami
Keogami

📖
Alexandre Esse
Alexandre Esse

🖋
Sagar Vora
Sagar Vora

🖋
Kacper Poneta
Kacper Poneta

🖋
Aaron Suggs
Aaron Suggs

🖋
Alex
Alex

🖋
Sebastian Törnquist
Sebastian Törnquist

🖋
Sebastian LaVine
Sebastian LaVine

💻
Alan Gerber
Alan Gerber

🖋
Eric
Eric

🖋
Aaron Wang
Aaron Wang

🖋
Noah
Noah

🖋
rb5014
rb5014

🖋
deedy5
deedy5

🖋
lionel-rowe
lionel-rowe

🖋
Ben
Ben

🖋
b1ue64
b1ue64

🖋
lazywalker
lazywalker

🖋
proofconstruction
proofconstruction

🚇
IVIURRAY
IVIURRAY

🖋
Bert Apperlo
Bert Apperlo

🖋
Florine W. Dekker
Florine W. Dekker

🖋
Mehul Gangavelli
Mehul Gangavelli

🖋
Mikael Frosthage
Mikael Frosthage

🖋
Robert Fry
Robert Fry

🖋
tajo48
tajo48

🖋
Anish
Anish

🖋
vnprc
vnprc

🖋
Joshua Carlson
Joshua Carlson

🖋
Nicholas R. Smith
Nicholas R. Smith

💻
Alexander González
Alexander González

🖋
Marcus Höjvall
Marcus Höjvall

🖋
Alon Hearter
Alon Hearter

🖋
shirts
shirts

🖋
Ivan Vasiunyk
Ivan Vasiunyk

🖋
Mo
Mo

💻
x10an14
x10an14

🚇
Roi Gabay
Roi Gabay

🖋
Máté Kovács
Máté Kovács

🖋
Gábor Szabó
Gábor Szabó

🖋
Yamila Moreno
Yamila Moreno

🖋
Will Hack
Will Hack

🖋
Michael
Michael

🖋
Mohammed Sadiq
Mohammed Sadiq

🖋
Jakob
Jakob

🖋
Oscar Bonilla
Oscar Bonilla

🖋
Jon Erling Hustadnes
Jon Erling Hustadnes

🖋
Charles Hall
Charles Hall

🚇
Luka Krmpotić
Luka Krmpotić

🖋
Jurglic
Jurglic

🖋
Ofir Lauber
Ofir Lauber

🖋
Chris Rose
Chris Rose

🚇
d1t2
d1t2

🚇
docwilco
docwilco

💻
Matt Nield
Matt Nield

🖋
The Bearodactyl
The Bearodactyl

💻
markgreene74
markgreene74

💻
Versha Dhankar
Versha Dhankar

📖
Tristram Oaten
Tristram Oaten

🖋
Daniel Tinazzi
Daniel Tinazzi

🖋
Raymon Roos
Raymon Roos

🖋
Matthias
Matthias

💻
J. Neuschäfer
J. Neuschäfer

💻
Bastian Pedersen
Bastian Pedersen

🖋
gerases
gerases

🖋
Luca Plian
Luca Plian

💻
René Reifenrath
René Reifenrath

🖋
Peter Neave
Peter Neave

🚇
Jan
Jan

🖋
Kyle VanderBeek
Kyle VanderBeek

🚇
pavedroad
pavedroad

🖋
luna
luna

🖋
Evan Miller
Evan Miller

🖋
luvchurchill
luvchurchill

💻
Ze-en Xiong
Ze-en Xiong

🖋
Parnav Harinathan
Parnav Harinathan

🖋
0Ahmed-0
0Ahmed-0

🖋
guizo792
guizo792

🖋
Kazuki Matsuo
Kazuki Matsuo

💻
Paul Leydier
Paul Leydier

📖
wznmickey
wznmickey

📖
NicolasRoelandt
NicolasRoelandt

📖
Josh Bouganim
Josh Bouganim

💻
Dan
Dan

💻
- - - - - - -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! From 09d8bc83ff2e7fe991dc72ab431a1cde8ee2c543 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 13:08:02 +0200 Subject: [PATCH 183/433] Remove README reference --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 96421ebd..9d097042 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,3 @@ Now you should be done! ## Contributing See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md). - -## Contributors ✨ - -Thanks goes to the wonderful people listed in [AUTHORS.md](https://github.com/rust-lang/rustlings/blob/main/AUTHORS.md) 🎉 From aaf183142e27e8a3232481dfd6bdc09e4403aeeb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 16:17:33 +0200 Subject: [PATCH 184/433] =?UTF-8?q?Bring=20back=20the=20thanks=20to=20cont?= =?UTF-8?q?ributors=20=E2=9D=A4=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9d097042..36e771b0 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,7 @@ Now you should be done! ## Contributing See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md). + +## Contributors ✨ + +Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉❤️ From daa090981aeafb24ed907cf85de49904ce17fc37 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 17:17:21 +0200 Subject: [PATCH 185/433] Update README --- README.md | 104 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 36e771b0..bfa3f06f 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,124 @@

-# rustlings 🦀❤️ +# Rustlings 🦀❤️
-Greetings and welcome to `rustlings`. This project contains small exercises to get you used to reading and writing Rust code. This includes reading and responding to compiler messages! +Greetings and welcome to Rustlings. +This project contains small exercises to get you used to reading and writing Rust code. +This includes reading and responding to compiler messages! -Alternatively, for a first-time Rust learner, there are several other resources: +It is recommended to do the Rustlings exercises in parallel to reading [the official Rust book](https://doc.rust-lang.org/book/), the most comprehensive resource for learning Rust 📚️ -- [The Book](https://doc.rust-lang.org/book/index.html) - The most comprehensive resource for learning Rust, but a bit theoretical sometimes. You will be using this along with Rustlings! -- [Rust By Example](https://doc.rust-lang.org/rust-by-example/index.html) - Learn Rust by solving little exercises! It's almost like `rustlings`, but online +[Rust By Example](https://doc.rust-lang.org/rust-by-example/) is another recommended resource that you might find helpful. +It contains code examples and exercises similar to Rustlings, but online. ## Getting Started -_Note: If you're on MacOS, make sure you've installed Xcode and its developer tools by typing `xcode-select --install`._ -_Note: If you're on Linux, make sure you've installed gcc. Deb: `sudo apt install gcc`. Yum: `sudo yum -y install gcc`._ +### Installing Rust -You will need to have Rust installed. You can get it by visiting . This'll also install Cargo, Rust's package/project manager. +Before installing Rustlings, you need to have _Rust installed_. +Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust. +This'll also install _Cargo_, Rust's package/project manager. - +🐧 If you're on Linux, make sure you've installed `gcc` (for a linker). Deb: `sudo apt install build-essential gcc`. Dnf: `sudo dnf install gcc`. + +🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by typing `xcode-select --install`. + +### Installing Rustlings + +The following command will download and compile Rustlings: + +```bash +cargo install rustlings --locked +``` + +### Initialization + +After installing Rustlings, run the following command to initialize the `rustlings/` directory: + +```bash +rustlings init +``` + +Now, go into the newly initialized directory and run Rustlings for further instructions on getting started with the exercises: + +```bash +cd rustlings/ +rustlings +``` ## Doing exercises -The exercises are sorted by topic and can be found in the subdirectory `rustlings/exercises/`. For every topic there is an additional README file with some resources to get you started on the topic. We really recommend that you have a look at them before you start. +The exercises are sorted by topic and can be found in the subdirectory `rustlings/exercises/`. +For every topic there is an additional README file with some resources to get you started on the topic. +We really recommend that you have a look at them before you start. -The task is simple. Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! Some exercises are also run as tests, but rustlings handles them all the same. To run the exercises in the recommended order, execute: +The task is simple. +Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! +Some exercises are also run as tests, but Rustlings handles them all the same. +To run the exercises in the recommended order, execute: ```bash -rustlings watch +rustlings ``` -This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). It will also rerun automatically every time you change a file in the `exercises/` directory. +This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). +It will also rerun automatically every time you change a file in the `exercises/` directory. In case you want to go by your own order, or want to only verify a single exercise, you can run: ```bash -rustlings run myExercise1 +rustlings run EXERCISE_NAME ``` -Or simply use the following command to run the next unsolved exercise in the course: +Or simply use the following command to run the next pending exercise in the course: ```bash -rustlings run next +rustlings run ``` -In case you get stuck, you can run the following command to get a hint for your -exercise: +In case you get stuck, you can run the following command to get a hint for your exercise: ```bash -rustlings hint myExercise1 +rustlings hint EXERCISE_NAME ``` -You can also get the hint for the next unsolved exercise with the following command: +You can also get the hint for the next pending exercise with the following command: ```bash -rustlings hint next +rustlings hint ``` -To check your progress, you can run the following command: +## Quizzes -```bash -rustlings list -``` - -## Testing yourself - -After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`. +After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. +These quizzes are found in `exercises/quizN.rs`. ## Continuing On -Once you've completed Rustlings, put your new knowledge to good use! Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. + + +Once you've completed Rustlings, put your new knowledge to good use! +Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. ## Uninstalling Rustlings -If you want to remove Rustlings from your system, there are two steps. First, you'll need to remove the exercises folder that the install script created -for you: +If you want to remove Rustlings from your system, there are two steps. + +1️⃣ Remove the `rustlings` directory that was created by `rustlings init`: ```bash -rm -rf rustlings # or your custom folder name, if you chose and or renamed it +rm -r rustlings ``` -Second, run `cargo uninstall` to remove the `rustlings` binary: +2️⃣ Run `cargo uninstall` to remove the `rustlings` binary: ```bash cargo uninstall rustlings ``` -Now you should be done! +That's it! ## Contributing @@ -94,4 +126,4 @@ See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUT ## Contributors ✨ -Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉❤️ +Thanks to [all the wonderful contributors](https://github.com/rust-lang/rustlings/graphs/contributors) 🎉 From a2be6754bf2833371fe99ddc2d01c0d518f8eb27 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 18 Apr 2024 17:17:39 +0200 Subject: [PATCH 186/433] Make the exercise name option for the hint subcommand --- src/main.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 04697379..4c5f1146 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,10 +63,10 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// Return a hint for the given exercise + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified. Hint { /// The name of the exercise - name: String, + name: Option, }, #[command(subcommand)] Dev(DevCommands), @@ -162,7 +162,9 @@ fn main() -> Result<()> { println!("The exercise {exercise_path} has been reset"); } Some(Subcommands::Hint { name }) => { - app_state.set_current_exercise_by_name(&name)?; + if let Some(name) = name { + app_state.set_current_exercise_by_name(&name)?; + } println!("{}", app_state.current_exercise().hint); } // Handled in an earlier match. From d83c91edc6365c90eb8d50be6f7036b38a64cba2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 18:20:15 +0200 Subject: [PATCH 187/433] Ignore all lock files but the one in root --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 80f9092a..945382c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Cargo target/ -/tests/fixture/*/Cargo.lock -/dev/Cargo.lock +Cargo.lock +!/Cargo.lock # State file .rustlings-state.txt From f1a60780b9d8cd7be544c3e09ddeb3834493c271 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 19:26:19 +0200 Subject: [PATCH 188/433] Rename constant --- src/dev.rs | 6 +++--- src/dev/check.rs | 4 ++-- src/dev/update.rs | 4 ++-- src/exercise.rs | 4 ++-- src/main.rs | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index 1430f11e..f2821811 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Context, Result}; use clap::Subcommand; -use crate::DEVELOPING_OFFICIAL_RUSTLINGS; +use crate::DEBUG_PROFILE; mod check; mod init; @@ -18,8 +18,8 @@ impl DevCommands { pub fn run(self) -> Result<()> { match self { DevCommands::Init => { - if DEVELOPING_OFFICIAL_RUSTLINGS { - bail!("Disabled while developing the official Rustlings"); + if DEBUG_PROFILE { + bail!("Disabled in the debug build"); } init::init().context(INIT_ERR) diff --git a/src/dev/check.rs b/src/dev/check.rs index e95eb3c6..cd115b71 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -8,7 +8,7 @@ use std::{ use crate::{ info_file::{ExerciseInfo, InfoFile}, - CURRENT_FORMAT_VERSION, DEVELOPING_OFFICIAL_RUSTLINGS, + CURRENT_FORMAT_VERSION, DEBUG_PROFILE, }; fn forbidden_char(input: &str) -> Option { @@ -193,7 +193,7 @@ pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; check_exercises(&info_file)?; - if DEVELOPING_OFFICIAL_RUSTLINGS { + if DEBUG_PROFILE { check_cargo_toml( &info_file.exercises, include_str!("../../dev/Cargo.toml"), diff --git a/src/dev/update.rs b/src/dev/update.rs index 65dcf768..1f032f79 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use crate::{ info_file::{ExerciseInfo, InfoFile}, - DEVELOPING_OFFICIAL_RUSTLINGS, + DEBUG_PROFILE, }; use super::check::{append_bins, bins_start_end_ind}; @@ -30,7 +30,7 @@ fn update_cargo_toml( pub fn update() -> Result<()> { let info_file = InfoFile::parse()?; - if DEVELOPING_OFFICIAL_RUSTLINGS { + if DEBUG_PROFILE { update_cargo_toml( &info_file.exercises, include_str!("../../dev/Cargo.toml"), diff --git a/src/exercise.rs b/src/exercise.rs index eb7b725e..e85efe4c 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -7,7 +7,7 @@ use std::{ process::{Command, Output}, }; -use crate::{info_file::Mode, DEVELOPING_OFFICIAL_RUSTLINGS}; +use crate::{info_file::Mode, DEBUG_PROFILE}; pub struct TerminalFileLink<'a> { path: &'a str, @@ -48,7 +48,7 @@ impl Exercise { cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - if DEVELOPING_OFFICIAL_RUSTLINGS && Path::new("tests").exists() { + if DEBUG_PROFILE && Path::new("tests").exists() { cmd.arg("--manifest-path").arg("dev/Cargo.toml"); } diff --git a/src/main.rs b/src/main.rs index 4c5f1146..69ac0a39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ mod run; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; -const DEVELOPING_OFFICIAL_RUSTLINGS: bool = { +const DEBUG_PROFILE: bool = { #[allow(unused_assignments, unused_mut)] let mut debug_profile = false; @@ -79,8 +79,8 @@ fn main() -> Result<()> { match args.command { Some(Subcommands::Init) => { - if DEVELOPING_OFFICIAL_RUSTLINGS { - bail!("Disabled while developing the official Rustlings"); + if DEBUG_PROFILE { + bail!("Disabled in the debug build"); } return init::init().context("Initialization failed"); From 04d36996dd745af284be380bd902e0bad491b87a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 19:27:00 +0200 Subject: [PATCH 189/433] Update deps --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b194de61..fdc79365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,9 +656,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" dependencies = [ "bitflags 2.5.0", "errno", @@ -771,9 +771,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.59" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -856,9 +856,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5e3be113..6181b1e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } serde = { version = "1.0.198", features = ["derive"] } -toml_edit = { version = "0.22.9", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.12", default-features = false, features = ["parse", "serde"] } which = "6.0.1" [dev-dependencies] From 49e4a1fab04560cf0e868ab8214dfc94e76b9f4b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 19:34:55 +0200 Subject: [PATCH 190/433] Catch the usage of the old method --- dev/rustlings-repo.txt | 1 + src/main.rs | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 dev/rustlings-repo.txt diff --git a/dev/rustlings-repo.txt b/dev/rustlings-repo.txt new file mode 100644 index 00000000..62793612 --- /dev/null +++ b/dev/rustlings-repo.txt @@ -0,0 +1 @@ +This file is used to check if the user tries to run Rustlings in the repository (the method before v6) diff --git a/src/main.rs b/src/main.rs index 69ac0a39..bf2498a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,6 +75,10 @@ enum Subcommands { fn main() -> Result<()> { let args = Args::parse(); + if !DEBUG_PROFILE && Path::new("dev/rustlings-repo.txt").exists() { + bail!("{OLD_METHOD_ERR}"); + } + which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; match args.command { @@ -174,6 +178,11 @@ fn main() -> Result<()> { Ok(()) } +const OLD_METHOD_ERR: &str = "You are trying to run Rustlings using the old method before v6. +The new method doesn't include cloning the Rustlings' repository. +Please follow the instructions in the README: +https://github.com/rust-lang/rustlings#getting-started"; + const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. Did you already install Rust? Try running `cargo --version` to diagnose the problem."; From 642c3bd37e3195f7f744a5fa60a53e59d8da5526 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 20:22:01 +0200 Subject: [PATCH 191/433] Fix the generated Cargo.toml after rustlings init --- dev/Cargo.toml | 2 +- src/cargo_toml.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++++ src/dev/check.rs | 51 +++++++++--------------------------------- src/dev/update.rs | 20 +++++++---------- src/init.rs | 26 +++++++++++---------- src/main.rs | 1 + 6 files changed, 91 insertions(+), 66 deletions(-) create mode 100644 src/cargo_toml.rs diff --git a/dev/Cargo.toml b/dev/Cargo.toml index e66973e0..eddf0164 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,4 +1,4 @@ -# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update` +# Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`. bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs new file mode 100644 index 00000000..2345a7ee --- /dev/null +++ b/src/cargo_toml.rs @@ -0,0 +1,57 @@ +use anyhow::{Context, Result}; + +use crate::info_file::ExerciseInfo; + +pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { + let start_ind = cargo_toml + .find("bin = [") + .context("Failed to find the start of the `bin` list (`bin = [`)")? + + 7; + let end_ind = start_ind + + cargo_toml + .get(start_ind..) + .and_then(|slice| slice.as_bytes().iter().position(|c| *c == b']')) + .context("Failed to find the end of the `bin` list (`]`)")?; + + Ok((start_ind, end_ind)) +} + +pub fn append_bins( + buf: &mut Vec, + exercise_infos: &[ExerciseInfo], + exercise_path_prefix: &[u8], +) { + buf.push(b'\n'); + for exercise_info in exercise_infos { + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"\", path = \""); + buf.extend_from_slice(exercise_path_prefix); + buf.extend_from_slice(b"exercises/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.push(b'/'); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); + } +} + +pub fn updated_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let mut updated_cargo_toml = Vec::with_capacity(1 << 13); + updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); + append_bins( + &mut updated_cargo_toml, + exercise_infos, + exercise_path_prefix, + ); + updated_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes()); + + Ok(updated_cargo_toml) +} diff --git a/src/dev/check.rs b/src/dev/check.rs index cd115b71..9859c3e3 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -7,6 +7,7 @@ use std::{ }; use crate::{ + cargo_toml::{append_bins, bins_start_end_ind}, info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, DEBUG_PROFILE, }; @@ -136,41 +137,6 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { Ok(()) } -pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { - let start_ind = cargo_toml - .find("bin = [") - .context("Failed to find the start of the `bin` list (`bin = [`)")? - + 7; - let end_ind = start_ind - + cargo_toml - .get(start_ind..) - .and_then(|slice| slice.as_bytes().iter().position(|c| *c == b']')) - .context("Failed to find the end of the `bin` list (`]`)")?; - - Ok((start_ind, end_ind)) -} - -pub fn append_bins( - buf: &mut Vec, - exercise_infos: &[ExerciseInfo], - exercise_path_prefix: &[u8], -) { - buf.push(b'\n'); - for exercise_info in exercise_infos { - buf.extend_from_slice(b" { name = \""); - buf.extend_from_slice(exercise_info.name.as_bytes()); - buf.extend_from_slice(b"\", path = \""); - buf.extend_from_slice(exercise_path_prefix); - buf.extend_from_slice(b"exercises/"); - if let Some(dir) = &exercise_info.dir { - buf.extend_from_slice(dir.as_bytes()); - buf.push(b'/'); - } - buf.extend_from_slice(exercise_info.name.as_bytes()); - buf.extend_from_slice(b".rs\" },\n"); - } -} - fn check_cargo_toml( exercise_infos: &[ExerciseInfo], current_cargo_toml: &str, @@ -183,7 +149,13 @@ fn check_cargo_toml( append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); if old_bins != new_bins { - bail!("`Cargo.toml` is outdated. Run `rustlings dev update` to update it"); + if DEBUG_PROFILE { + bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + } else { + bail!( + "The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it", + ); + } } Ok(()) @@ -198,14 +170,11 @@ pub fn check() -> Result<()> { &info_file.exercises, include_str!("../../dev/Cargo.toml"), b"../", - ) - .context("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it")?; + )?; } else { let current_cargo_toml = fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; - check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"").context( - "The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it", - )?; + check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; } println!("\nEverything looks fine!"); diff --git a/src/dev/update.rs b/src/dev/update.rs index 1f032f79..d2f20aae 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -3,26 +3,22 @@ use std::fs; use anyhow::{Context, Result}; use crate::{ + cargo_toml::updated_cargo_toml, info_file::{ExerciseInfo, InfoFile}, DEBUG_PROFILE, }; -use super::check::{append_bins, bins_start_end_ind}; - fn update_cargo_toml( exercise_infos: &[ExerciseInfo], current_cargo_toml: &str, - cargo_toml_path: &str, exercise_path_prefix: &[u8], + cargo_toml_path: &str, ) -> Result<()> { - let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + let updated_cargo_toml = + updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?; - let mut new_cargo_toml = Vec::with_capacity(1 << 13); - new_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); - append_bins(&mut new_cargo_toml, exercise_infos, exercise_path_prefix); - new_cargo_toml.extend_from_slice(current_cargo_toml[bins_end_ind..].as_bytes()); - - fs::write(cargo_toml_path, new_cargo_toml).context("Failed to write the `Cargo.toml` file")?; + fs::write(cargo_toml_path, updated_cargo_toml) + .context("Failed to write the `Cargo.toml` file")?; Ok(()) } @@ -34,8 +30,8 @@ pub fn update() -> Result<()> { update_cargo_toml( &info_file.exercises, include_str!("../../dev/Cargo.toml"), - "dev/Cargo.toml", b"../", + "dev/Cargo.toml", ) .context("Failed to update the file `dev/Cargo.toml`")?; @@ -43,7 +39,7 @@ pub fn update() -> Result<()> { } else { let current_cargo_toml = fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?; - update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, "Cargo.toml", b"") + update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"", "Cargo.toml") .context("Failed to update the file `Cargo.toml`")?; println!("Updated `Cargo.toml`"); diff --git a/src/init.rs b/src/init.rs index 52315e24..f210db77 100644 --- a/src/init.rs +++ b/src/init.rs @@ -6,17 +6,7 @@ use std::{ path::Path, }; -use crate::embedded::EMBEDDED_FILES; - -const CARGO_TOML: &[u8] = { - let cargo_toml = include_bytes!("../dev/Cargo.toml"); - // Skip the first line (comment). - let mut start_ind = 0; - while cargo_toml[start_ind] != b'\n' { - start_ind += 1; - } - cargo_toml.split_at(start_ind + 1).1 -}; +use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile}; pub fn init() -> Result<()> { if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { @@ -38,7 +28,19 @@ pub fn init() -> Result<()> { .init_exercises_dir() .context("Failed to initialize the `rustlings/exercises` directory")?; - fs::write("Cargo.toml", CARGO_TOML) + let info_file = InfoFile::parse()?; + let current_cargo_toml = include_str!("../dev/Cargo.toml"); + // Skip the first line (comment). + let newline_ind = current_cargo_toml + .as_bytes() + .iter() + .position(|c| *c == b'\n') + .context("The embedded `Cargo.toml` is empty or contains only one line.")?; + let current_cargo_toml = + ¤t_cargo_toml[(newline_ind + 1).min(current_cargo_toml.len() - 1)..]; + let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"") + .context("Failed to generate `Cargo.toml`")?; + fs::write("Cargo.toml", updated_cargo_toml) .context("Failed to create the file `rustlings/Cargo.toml`")?; fs::write(".gitignore", GITIGNORE) diff --git a/src/main.rs b/src/main.rs index bf2498a9..494c40d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use std::{ use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; mod app_state; +mod cargo_toml; mod dev; mod embedded; mod exercise; From e3b9124b85502d21b8d45abe8e4e7e4d3e08e7be Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 23:24:10 +0200 Subject: [PATCH 192/433] Add a confirmation prompt to the init subcommand --- src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main.rs b/src/main.rs index 494c40d4..f52699c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,6 +88,14 @@ fn main() -> Result<()> { bail!("Disabled in the debug build"); } + { + let mut stdout = io::stdout().lock(); + stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; + stdout.flush()?; + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + stdout.write_all(b"\n")?; + } + return init::init().context("Initialization failed"); } Some(Subcommands::Dev(dev_command)) => return dev_command.run(), From 30040d77781e03043e72d09d7fe8cf1cf5436a9c Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 23:39:44 +0200 Subject: [PATCH 193/433] Add a disclaimer to the state file --- src/app_state.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 98687818..09de2a3e 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -53,7 +53,8 @@ impl AppState { } // See `Self::write` for more information about the file format. - let mut lines = self.file_buf.split(|c| *c == b'\n'); + let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2); + let Some(current_exercise_name) = lines.next() else { return StateFileStatus::NotRead; }; @@ -300,13 +301,17 @@ impl AppState { // Write the state file. // The file's format is very simple: - // - The first line is the name of the current exercise. It must end with `\n` even if there - // are no done exercises. + // - The first line is a comment. // - The second line is an empty line. + // - The third line is the name of the current exercise. It must end with `\n` even if there + // are no done exercises. + // - The fourth line is an empty line. // - All remaining lines are the names of done exercises. fn write(&mut self) -> Result<()> { self.file_buf.clear(); + self.file_buf + .extend_from_slice(b"DON'T EDIT THIS FILE!\n\n"); self.file_buf .extend_from_slice(self.current_exercise().name.as_bytes()); self.file_buf.push(b'\n'); From 61a84a2c118af05493c86862e2eb5dbf7977a02e Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 21 Apr 2024 23:43:49 +0200 Subject: [PATCH 194/433] dev init -> dev new PATH --- src/dev.rs | 8 ++++---- src/dev/{init.rs => new.rs} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename src/dev/{init.rs => new.rs} (100%) diff --git a/src/dev.rs b/src/dev.rs index f2821811..a55a95e1 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -4,12 +4,12 @@ use clap::Subcommand; use crate::DEBUG_PROFILE; mod check; -mod init; +mod new; mod update; #[derive(Subcommand)] pub enum DevCommands { - Init, + New { path: String }, Check, Update, } @@ -17,12 +17,12 @@ pub enum DevCommands { impl DevCommands { pub fn run(self) -> Result<()> { match self { - DevCommands::Init => { + DevCommands::New { path } => { if DEBUG_PROFILE { bail!("Disabled in the debug build"); } - init::init().context(INIT_ERR) + new::init().context(INIT_ERR) } DevCommands::Check => check::check(), DevCommands::Update => update::update(), diff --git a/src/dev/init.rs b/src/dev/new.rs similarity index 100% rename from src/dev/init.rs rename to src/dev/new.rs From e93a99e19e5f1a5dee6b28ed2f703bff57038b11 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 22 Apr 2024 00:34:55 +0200 Subject: [PATCH 195/433] Third-party exercises should be in a separate Git repo --- src/dev.rs | 6 ++-- src/dev/new.rs | 93 +++++++++++++++++++++++++++++++------------------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index a55a95e1..68777d10 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use anyhow::{bail, Context, Result}; use clap::Subcommand; @@ -9,7 +11,7 @@ mod update; #[derive(Subcommand)] pub enum DevCommands { - New { path: String }, + New { path: PathBuf }, Check, Update, } @@ -22,7 +24,7 @@ impl DevCommands { bail!("Disabled in the debug build"); } - new::init().context(INIT_ERR) + new::new(&path).context(INIT_ERR) } DevCommands::Check => check::check(), DevCommands::Update => update::update(), diff --git a/src/dev/new.rs b/src/dev/new.rs index 3ce5055c..b0828a4b 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -1,41 +1,72 @@ -use anyhow::{Context, Result}; -use std::fs::{self, create_dir}; +use anyhow::{bail, Context, Result}; +use std::{ + env::set_current_dir, + fs::{self, create_dir}, + path::Path, + process::Command, +}; use crate::CURRENT_FORMAT_VERSION; -pub fn init() -> Result<()> { - create_dir("rustlings").context("Failed to create the directory `rustlings`")?; +fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { + create_dir(dir_name) + .with_context(|| format!("Failed to create the directory {current_dir}/{dir_name}"))?; + println!("Created the directory {current_dir}/{dir_name}"); + Ok(()) +} - create_dir("rustlings/exercises") - .context("Failed to create the directory `rustlings/exercises`")?; +fn write_rel_file(file_name: &str, current_dir: &str, content: C) -> Result<()> +where + C: AsRef<[u8]>, +{ + fs::write(file_name, content) + .with_context(|| format!("Failed to create the file {current_dir}/{file_name}"))?; + // Space to align with `create_rel_dir`. + println!("Created the file {current_dir}/{file_name}"); + Ok(()) +} - create_dir("rustlings/solutions") - .context("Failed to create the directory `rustlings/solutions`")?; +pub fn new(path: &Path) -> Result<()> { + let dir_name = path.to_string_lossy(); - fs::write( - "rustlings/info.toml", + create_dir(path).with_context(|| format!("Failed to create the directory {dir_name}"))?; + println!("Created the directory {dir_name}"); + + set_current_dir(path) + .with_context(|| format!("Failed to set {dir_name} as the current directory"))?; + + if !Command::new("git") + .arg("init") + .status() + .context("Failed to run `git init`")? + .success() + { + bail!("`git init` didn't run successfully. See the error message above"); + } + + write_rel_file(".gitignore", &dir_name, crate::init::GITIGNORE)?; + + create_rel_dir("exercises", &dir_name)?; + create_rel_dir("solutions", &dir_name)?; + + write_rel_file( + "info.toml", + &dir_name, format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"), - ) - .context("Failed to create the file `rustlings/info.toml`")?; + )?; - fs::write("rustlings/Cargo.toml", CARGO_TOML) - .context("Failed to create the file `rustlings/Cargo.toml`")?; + write_rel_file("Cargo.toml", &dir_name, CARGO_TOML)?; - fs::write("rustlings/.gitignore", crate::init::GITIGNORE) - .context("Failed to create the file `rustlings/.gitignore`")?; + write_rel_file("README.md", &dir_name, README)?; - fs::write("rustlings/README.md", README) - .context("Failed to create the file `rustlings/README.md`")?; - - create_dir("rustlings/.vscode") - .context("Failed to create the directory `rustlings/.vscode`")?; - fs::write( - "rustlings/.vscode/extensions.json", + create_rel_dir(".vscode", &dir_name)?; + write_rel_file( + ".vscode/extensions.json", + &dir_name, crate::init::VS_CODE_EXTENSIONS_JSON, - ) - .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + )?; - println!("{INIT_DONE}"); + println!("\nInitialization done ✓"); Ok(()) } @@ -97,13 +128,3 @@ First, Then, open your terminal in this directory and run `rustlings` to get started with the exercises 🚀 "; - -const INIT_DONE: &str = r#"Initialization done! -You can start developing third-party Rustlings exercises in the `rustlings` directory :D - -If the initialization was done in a Rust project which is a Cargo workspace, you need to add the -path to the `rustlings` directory to the `workspace.exclude` list in the project's `Cargo.toml` -file. For example: - -[workspace] -exclude = ["rustlings"]"#; From 4ce2714da1f079e81b6887b52b1acfbc283a3d63 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 22 Apr 2024 00:38:34 +0200 Subject: [PATCH 196/433] Add --no-git --- src/dev.rs | 10 +++++++--- src/dev/new.rs | 13 +++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index 68777d10..d7f9af65 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -11,7 +11,11 @@ mod update; #[derive(Subcommand)] pub enum DevCommands { - New { path: PathBuf }, + New { + path: PathBuf, + #[arg(long)] + no_git: bool, + }, Check, Update, } @@ -19,12 +23,12 @@ pub enum DevCommands { impl DevCommands { pub fn run(self) -> Result<()> { match self { - DevCommands::New { path } => { + DevCommands::New { path, no_git } => { if DEBUG_PROFILE { bail!("Disabled in the debug build"); } - new::new(&path).context(INIT_ERR) + new::new(&path, no_git).context(INIT_ERR) } DevCommands::Check => check::check(), DevCommands::Update => update::update(), diff --git a/src/dev/new.rs b/src/dev/new.rs index b0828a4b..82aba42b 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -26,7 +26,7 @@ where Ok(()) } -pub fn new(path: &Path) -> Result<()> { +pub fn new(path: &Path, no_git: bool) -> Result<()> { let dir_name = path.to_string_lossy(); create_dir(path).with_context(|| format!("Failed to create the directory {dir_name}"))?; @@ -35,11 +35,12 @@ pub fn new(path: &Path) -> Result<()> { set_current_dir(path) .with_context(|| format!("Failed to set {dir_name} as the current directory"))?; - if !Command::new("git") - .arg("init") - .status() - .context("Failed to run `git init`")? - .success() + if !no_git + && !Command::new("git") + .arg("init") + .status() + .context("Failed to run `git init`")? + .success() { bail!("`git init` didn't run successfully. See the error message above"); } From 86684b7fc9dd5e8bedad6056565645d1d980925c Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 22 Apr 2024 00:45:16 +0200 Subject: [PATCH 197/433] Document dev commands --- src/dev.rs | 5 +++++ src/main.rs | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index d7f9af65..38338cd0 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -11,12 +11,17 @@ mod update; #[derive(Subcommand)] pub enum DevCommands { + /// Create a new project for third-party Rustlings exercises New { + /// The path to create the project in path: PathBuf, + /// Don't initialize a Git repository in the project directory #[arg(long)] no_git: bool, }, + /// Run checks on the exercises Check, + /// Update the `Cargo.toml` file for the exercises Update, } diff --git a/src/main.rs b/src/main.rs index f52699c7..9ff218a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,7 +54,7 @@ struct Args { enum Subcommands { /// Initialize Rustlings Init, - /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified. + /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified Run { /// The name of the exercise name: Option, @@ -64,11 +64,12 @@ enum Subcommands { /// The name of the exercise name: String, }, - /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified. + /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified Hint { /// The name of the exercise name: Option, }, + /// Commands for developing (third-party) Rustlings exercises #[command(subcommand)] Dev(DevCommands), } From ad8e5444837b5c2b06497b9b592fbbb8c2db057e Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 22 Apr 2024 01:07:36 +0200 Subject: [PATCH 198/433] Move quizzes --- README.md | 2 +- exercises/{ => quizzes}/quiz1.rs | 0 exercises/{ => quizzes}/quiz2.rs | 0 exercises/{ => quizzes}/quiz3.rs | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename exercises/{ => quizzes}/quiz1.rs (100%) rename exercises/{ => quizzes}/quiz2.rs (100%) rename exercises/{ => quizzes}/quiz3.rs (100%) diff --git a/README.md b/README.md index bfa3f06f..84125a01 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ rustlings hint ## Quizzes After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. -These quizzes are found in `exercises/quizN.rs`. +These quizzes are found in `exercises/quizzes`. ## Continuing On diff --git a/exercises/quiz1.rs b/exercises/quizzes/quiz1.rs similarity index 100% rename from exercises/quiz1.rs rename to exercises/quizzes/quiz1.rs diff --git a/exercises/quiz2.rs b/exercises/quizzes/quiz2.rs similarity index 100% rename from exercises/quiz2.rs rename to exercises/quizzes/quiz2.rs diff --git a/exercises/quiz3.rs b/exercises/quizzes/quiz3.rs similarity index 100% rename from exercises/quiz3.rs rename to exercises/quizzes/quiz3.rs From 5349f0e8d4ea115861d7ba4d9f1f54a9b096a055 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 23 Apr 2024 15:32:01 +0200 Subject: [PATCH 199/433] Add README to the quizzes directory --- README.md | 5 ----- exercises/quizzes/README.md | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 exercises/quizzes/README.md diff --git a/README.md b/README.md index 84125a01..2164269f 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,6 @@ You can also get the hint for the next pending exercise with the following comma rustlings hint ``` -## Quizzes - -After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. -These quizzes are found in `exercises/quizzes`. - ## Continuing On diff --git a/exercises/quizzes/README.md b/exercises/quizzes/README.md new file mode 100644 index 00000000..4d3bcd94 --- /dev/null +++ b/exercises/quizzes/README.md @@ -0,0 +1,3 @@ +# Quizzes + +After every couple of sections, there will be a quiz in this directory that'll test your knowledge on a bunch of sections at once. From e5a19a4c33e16e517e4d597acb721ed281c7bdae Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 23 Apr 2024 15:32:07 +0200 Subject: [PATCH 200/433] Update deps --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdc79365..1618eec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,9 +656,9 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustix" -version = "0.38.33" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", From 2dac8e509bed07c30a98983cfb6b80f73a1582e9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 23 Apr 2024 19:18:25 +0200 Subject: [PATCH 201/433] Refactor embedded files to add solutions --- Cargo.lock | 2 + Cargo.toml | 8 +- info.toml | 3 + rustlings-macros/Cargo.toml | 2 + rustlings-macros/src/lib.rs | 97 ++++----------- solutions/00_intro/intro1.rs | 1 + solutions/00_intro/intro2.rs | 1 + solutions/01_variables/variables1.rs | 1 + solutions/01_variables/variables2.rs | 1 + solutions/01_variables/variables3.rs | 1 + solutions/01_variables/variables4.rs | 1 + solutions/01_variables/variables5.rs | 1 + solutions/01_variables/variables6.rs | 1 + solutions/02_functions/functions1.rs | 1 + solutions/02_functions/functions2.rs | 1 + solutions/02_functions/functions3.rs | 1 + solutions/02_functions/functions4.rs | 1 + solutions/02_functions/functions5.rs | 1 + solutions/03_if/if1.rs | 1 + solutions/03_if/if2.rs | 1 + solutions/03_if/if3.rs | 1 + .../04_primitive_types/primitive_types1.rs | 1 + .../04_primitive_types/primitive_types2.rs | 1 + .../04_primitive_types/primitive_types3.rs | 1 + .../04_primitive_types/primitive_types4.rs | 1 + .../04_primitive_types/primitive_types5.rs | 1 + .../04_primitive_types/primitive_types6.rs | 1 + solutions/05_vecs/vecs1.rs | 1 + solutions/05_vecs/vecs2.rs | 1 + .../06_move_semantics/move_semantics1.rs | 1 + .../06_move_semantics/move_semantics2.rs | 1 + .../06_move_semantics/move_semantics3.rs | 1 + .../06_move_semantics/move_semantics4.rs | 1 + .../06_move_semantics/move_semantics5.rs | 1 + .../06_move_semantics/move_semantics6.rs | 1 + solutions/07_structs/structs1.rs | 1 + solutions/07_structs/structs2.rs | 1 + solutions/07_structs/structs3.rs | 1 + solutions/08_enums/enums1.rs | 1 + solutions/08_enums/enums2.rs | 1 + solutions/08_enums/enums3.rs | 1 + solutions/09_strings/strings1.rs | 1 + solutions/09_strings/strings2.rs | 1 + solutions/09_strings/strings3.rs | 1 + solutions/09_strings/strings4.rs | 1 + solutions/10_modules/modules1.rs | 1 + solutions/10_modules/modules2.rs | 1 + solutions/10_modules/modules3.rs | 1 + solutions/11_hashmaps/hashmaps1.rs | 1 + solutions/11_hashmaps/hashmaps2.rs | 1 + solutions/11_hashmaps/hashmaps3.rs | 1 + solutions/12_options/options1.rs | 1 + solutions/12_options/options2.rs | 1 + solutions/12_options/options3.rs | 1 + solutions/13_error_handling/errors1.rs | 1 + solutions/13_error_handling/errors2.rs | 1 + solutions/13_error_handling/errors3.rs | 1 + solutions/13_error_handling/errors4.rs | 1 + solutions/13_error_handling/errors5.rs | 1 + solutions/13_error_handling/errors6.rs | 1 + solutions/14_generics/generics1.rs | 1 + solutions/14_generics/generics2.rs | 1 + solutions/15_traits/traits1.rs | 1 + solutions/15_traits/traits2.rs | 1 + solutions/15_traits/traits3.rs | 1 + solutions/15_traits/traits4.rs | 1 + solutions/15_traits/traits5.rs | 1 + solutions/16_lifetimes/lifetimes1.rs | 1 + solutions/16_lifetimes/lifetimes2.rs | 1 + solutions/16_lifetimes/lifetimes3.rs | 1 + solutions/17_tests/tests1.rs | 1 + solutions/17_tests/tests2.rs | 1 + solutions/17_tests/tests3.rs | 1 + solutions/17_tests/tests4.rs | 1 + solutions/18_iterators/iterators1.rs | 1 + solutions/18_iterators/iterators2.rs | 1 + solutions/18_iterators/iterators3.rs | 1 + solutions/18_iterators/iterators4.rs | 1 + solutions/18_iterators/iterators5.rs | 1 + solutions/19_smart_pointers/arc1.rs | 1 + solutions/19_smart_pointers/box1.rs | 1 + solutions/19_smart_pointers/cow1.rs | 1 + solutions/19_smart_pointers/rc1.rs | 1 + solutions/20_threads/threads1.rs | 1 + solutions/20_threads/threads2.rs | 1 + solutions/20_threads/threads3.rs | 1 + solutions/21_macros/macros1.rs | 1 + solutions/21_macros/macros2.rs | 1 + solutions/21_macros/macros3.rs | 1 + solutions/21_macros/macros4.rs | 1 + solutions/22_clippy/clippy1.rs | 1 + solutions/22_clippy/clippy2.rs | 1 + solutions/22_clippy/clippy3.rs | 1 + solutions/23_conversions/as_ref_mut.rs | 1 + solutions/23_conversions/from_into.rs | 1 + solutions/23_conversions/from_str.rs | 1 + solutions/23_conversions/try_from_into.rs | 1 + solutions/23_conversions/using_as.rs | 1 + solutions/quizzes/quiz1.rs | 1 + solutions/quizzes/quiz2.rs | 1 + solutions/quizzes/quiz3.rs | 1 + src/app_state.rs | 33 +++--- src/embedded.rs | 110 +++++++----------- src/exercise.rs | 1 + src/init.rs | 4 +- 105 files changed, 198 insertions(+), 158 deletions(-) create mode 100644 solutions/00_intro/intro1.rs create mode 100644 solutions/00_intro/intro2.rs create mode 100644 solutions/01_variables/variables1.rs create mode 100644 solutions/01_variables/variables2.rs create mode 100644 solutions/01_variables/variables3.rs create mode 100644 solutions/01_variables/variables4.rs create mode 100644 solutions/01_variables/variables5.rs create mode 100644 solutions/01_variables/variables6.rs create mode 100644 solutions/02_functions/functions1.rs create mode 100644 solutions/02_functions/functions2.rs create mode 100644 solutions/02_functions/functions3.rs create mode 100644 solutions/02_functions/functions4.rs create mode 100644 solutions/02_functions/functions5.rs create mode 100644 solutions/03_if/if1.rs create mode 100644 solutions/03_if/if2.rs create mode 100644 solutions/03_if/if3.rs create mode 100644 solutions/04_primitive_types/primitive_types1.rs create mode 100644 solutions/04_primitive_types/primitive_types2.rs create mode 100644 solutions/04_primitive_types/primitive_types3.rs create mode 100644 solutions/04_primitive_types/primitive_types4.rs create mode 100644 solutions/04_primitive_types/primitive_types5.rs create mode 100644 solutions/04_primitive_types/primitive_types6.rs create mode 100644 solutions/05_vecs/vecs1.rs create mode 100644 solutions/05_vecs/vecs2.rs create mode 100644 solutions/06_move_semantics/move_semantics1.rs create mode 100644 solutions/06_move_semantics/move_semantics2.rs create mode 100644 solutions/06_move_semantics/move_semantics3.rs create mode 100644 solutions/06_move_semantics/move_semantics4.rs create mode 100644 solutions/06_move_semantics/move_semantics5.rs create mode 100644 solutions/06_move_semantics/move_semantics6.rs create mode 100644 solutions/07_structs/structs1.rs create mode 100644 solutions/07_structs/structs2.rs create mode 100644 solutions/07_structs/structs3.rs create mode 100644 solutions/08_enums/enums1.rs create mode 100644 solutions/08_enums/enums2.rs create mode 100644 solutions/08_enums/enums3.rs create mode 100644 solutions/09_strings/strings1.rs create mode 100644 solutions/09_strings/strings2.rs create mode 100644 solutions/09_strings/strings3.rs create mode 100644 solutions/09_strings/strings4.rs create mode 100644 solutions/10_modules/modules1.rs create mode 100644 solutions/10_modules/modules2.rs create mode 100644 solutions/10_modules/modules3.rs create mode 100644 solutions/11_hashmaps/hashmaps1.rs create mode 100644 solutions/11_hashmaps/hashmaps2.rs create mode 100644 solutions/11_hashmaps/hashmaps3.rs create mode 100644 solutions/12_options/options1.rs create mode 100644 solutions/12_options/options2.rs create mode 100644 solutions/12_options/options3.rs create mode 100644 solutions/13_error_handling/errors1.rs create mode 100644 solutions/13_error_handling/errors2.rs create mode 100644 solutions/13_error_handling/errors3.rs create mode 100644 solutions/13_error_handling/errors4.rs create mode 100644 solutions/13_error_handling/errors5.rs create mode 100644 solutions/13_error_handling/errors6.rs create mode 100644 solutions/14_generics/generics1.rs create mode 100644 solutions/14_generics/generics2.rs create mode 100644 solutions/15_traits/traits1.rs create mode 100644 solutions/15_traits/traits2.rs create mode 100644 solutions/15_traits/traits3.rs create mode 100644 solutions/15_traits/traits4.rs create mode 100644 solutions/15_traits/traits5.rs create mode 100644 solutions/16_lifetimes/lifetimes1.rs create mode 100644 solutions/16_lifetimes/lifetimes2.rs create mode 100644 solutions/16_lifetimes/lifetimes3.rs create mode 100644 solutions/17_tests/tests1.rs create mode 100644 solutions/17_tests/tests2.rs create mode 100644 solutions/17_tests/tests3.rs create mode 100644 solutions/17_tests/tests4.rs create mode 100644 solutions/18_iterators/iterators1.rs create mode 100644 solutions/18_iterators/iterators2.rs create mode 100644 solutions/18_iterators/iterators3.rs create mode 100644 solutions/18_iterators/iterators4.rs create mode 100644 solutions/18_iterators/iterators5.rs create mode 100644 solutions/19_smart_pointers/arc1.rs create mode 100644 solutions/19_smart_pointers/box1.rs create mode 100644 solutions/19_smart_pointers/cow1.rs create mode 100644 solutions/19_smart_pointers/rc1.rs create mode 100644 solutions/20_threads/threads1.rs create mode 100644 solutions/20_threads/threads2.rs create mode 100644 solutions/20_threads/threads3.rs create mode 100644 solutions/21_macros/macros1.rs create mode 100644 solutions/21_macros/macros2.rs create mode 100644 solutions/21_macros/macros3.rs create mode 100644 solutions/21_macros/macros4.rs create mode 100644 solutions/22_clippy/clippy1.rs create mode 100644 solutions/22_clippy/clippy2.rs create mode 100644 solutions/22_clippy/clippy3.rs create mode 100644 solutions/23_conversions/as_ref_mut.rs create mode 100644 solutions/23_conversions/from_into.rs create mode 100644 solutions/23_conversions/from_str.rs create mode 100644 solutions/23_conversions/try_from_into.rs create mode 100644 solutions/23_conversions/using_as.rs create mode 100644 solutions/quizzes/quiz1.rs create mode 100644 solutions/quizzes/quiz2.rs create mode 100644 solutions/quizzes/quiz3.rs diff --git a/Cargo.lock b/Cargo.lock index 1618eec1..93617fab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,8 @@ name = "rustlings-macros" version = "6.0.0-alpha.0" dependencies = [ "quote", + "serde", + "toml_edit", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6181b1e9..1e6a24ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,10 @@ authors = [ license = "MIT" edition = "2021" +[workspace.dependencies] +serde = { version = "1.0.198", features = ["derive"] } +toml_edit = { version = "0.22.12", default-features = false, features = ["parse", "serde"] } + [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" @@ -41,8 +45,8 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } -serde = { version = "1.0.198", features = ["derive"] } -toml_edit = { version = "0.22.12", default-features = false, features = ["parse", "serde"] } +serde.workspace = true +toml_edit.workspace = true which = "6.0.1" [dev-dependencies] diff --git a/info.toml b/info.toml index 27071a5c..6549e1c4 100644 --- a/info.toml +++ b/info.toml @@ -236,6 +236,7 @@ Make sure the type is consistent across all arms.""" [[exercises]] name = "quiz1" +dir = "quizzes" mode = "test" hint = "No hints this time ;)" @@ -637,6 +638,7 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v [[exercises]] name = "quiz2" +dir = "quizzes" mode = "test" hint = "No hints this time ;)" @@ -870,6 +872,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe [[exercises]] name = "quiz3" +dir = "quizzes" mode = "test" hint = """ To find the best solution to this challenge you're going to need to think back diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index 095deaa3..f925f692 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -11,3 +11,5 @@ proc-macro = true [dependencies] quote = "1.0.36" +serde.workspace = true +toml_edit.workspace = true diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index d95a93a6..0bf3dbdf 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -1,86 +1,39 @@ use proc_macro::TokenStream; use quote::quote; -use std::{fs::read_dir, panic, path::PathBuf}; +use serde::Deserialize; -fn path_to_string(path: PathBuf) -> String { - path.into_os_string() - .into_string() - .unwrap_or_else(|original| { - panic!("The path {} is invalid UTF8", original.to_string_lossy()); - }) +#[derive(Deserialize)] +struct ExerciseInfo { + name: String, + dir: String, +} + +#[derive(Deserialize)] +struct InfoFile { + exercises: Vec, } #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - let mut files = Vec::with_capacity(8); - let mut dirs = Vec::with_capacity(128); + let exercises = toml_edit::de::from_str::(include_str!("../../info.toml")) + .expect("Failed to parse `info.toml`") + .exercises; - for entry in read_dir("exercises").expect("Failed to open the `exercises` directory") { - let entry = entry.expect("Failed to read the `exercises` directory"); - - if entry.file_type().unwrap().is_file() { - let path = entry.path(); - if path.file_name().unwrap() != "README.md" { - files.push(path_to_string(path)); - } - - continue; - } - - let dir_path = entry.path(); - let dir_files = read_dir(&dir_path).unwrap_or_else(|e| { - panic!("Failed to open the directory {}: {e}", dir_path.display()); - }); - let dir_path = path_to_string(dir_path); - let dir_files = dir_files.filter_map(|entry| { - let entry = entry.unwrap_or_else(|e| { - panic!("Failed to read the directory {dir_path}: {e}"); - }); - let path = entry.path(); - - if !entry.file_type().unwrap().is_file() { - panic!("Found {} but expected only files", path.display()); - } - - if path.file_name().unwrap() == "README.md" { - return None; - } - - Some(path_to_string(path)) - }); - - dirs.push(quote! { - EmbeddedFlatDir { - path: #dir_path, - readme: EmbeddedFile { - path: ::std::concat!(#dir_path, "/README.md"), - content: ::std::include_bytes!(::std::concat!("../", #dir_path, "/README.md")), - }, - content: &[ - #(EmbeddedFile { - path: #dir_files, - content: ::std::include_bytes!(::std::concat!("../", #dir_files)), - }),* - ], - } - }); - } + let exercise_files = exercises + .iter() + .map(|exercise| format!("../exercises/{}/{}.rs", exercise.dir, exercise.name)); + let solution_files = exercises + .iter() + .map(|exercise| format!("../solutions/{}/{}.rs", exercise.dir, exercise.name)); + let dirs = exercises.iter().map(|exercise| &exercise.dir); + let readmes = exercises + .iter() + .map(|exercise| format!("../exercises/{}/README.md", exercise.dir)); quote! { EmbeddedFiles { - exercises_dir: ExercisesDir { - readme: EmbeddedFile { - path: "exercises/README.md", - content: ::std::include_bytes!("../exercises/README.md"), - }, - files: &[#( - EmbeddedFile { - path: #files, - content: ::std::include_bytes!(::std::concat!("../", #files)), - } - ),*], - dirs: &[#(#dirs),*], - }, + exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files) }),*], + exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*] } } .into() diff --git a/solutions/00_intro/intro1.rs b/solutions/00_intro/intro1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/00_intro/intro1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/00_intro/intro2.rs b/solutions/00_intro/intro2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/00_intro/intro2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/01_variables/variables1.rs b/solutions/01_variables/variables1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/01_variables/variables1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/01_variables/variables2.rs b/solutions/01_variables/variables2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/01_variables/variables2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/01_variables/variables3.rs b/solutions/01_variables/variables3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/01_variables/variables3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/01_variables/variables4.rs b/solutions/01_variables/variables4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/01_variables/variables4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/01_variables/variables5.rs b/solutions/01_variables/variables5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/01_variables/variables5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/01_variables/variables6.rs b/solutions/01_variables/variables6.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/01_variables/variables6.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/02_functions/functions1.rs b/solutions/02_functions/functions1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/02_functions/functions1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/02_functions/functions2.rs b/solutions/02_functions/functions2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/02_functions/functions2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/02_functions/functions3.rs b/solutions/02_functions/functions3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/02_functions/functions3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/02_functions/functions4.rs b/solutions/02_functions/functions4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/02_functions/functions4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/02_functions/functions5.rs b/solutions/02_functions/functions5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/02_functions/functions5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/03_if/if1.rs b/solutions/03_if/if1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/03_if/if1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/03_if/if2.rs b/solutions/03_if/if2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/03_if/if2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/03_if/if3.rs b/solutions/03_if/if3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/03_if/if3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/04_primitive_types/primitive_types1.rs b/solutions/04_primitive_types/primitive_types1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/04_primitive_types/primitive_types2.rs b/solutions/04_primitive_types/primitive_types2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/04_primitive_types/primitive_types3.rs b/solutions/04_primitive_types/primitive_types3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/04_primitive_types/primitive_types4.rs b/solutions/04_primitive_types/primitive_types4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/04_primitive_types/primitive_types5.rs b/solutions/04_primitive_types/primitive_types5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/04_primitive_types/primitive_types6.rs b/solutions/04_primitive_types/primitive_types6.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/04_primitive_types/primitive_types6.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/05_vecs/vecs1.rs b/solutions/05_vecs/vecs1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/05_vecs/vecs1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/05_vecs/vecs2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/06_move_semantics/move_semantics1.rs b/solutions/06_move_semantics/move_semantics1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/06_move_semantics/move_semantics2.rs b/solutions/06_move_semantics/move_semantics2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/06_move_semantics/move_semantics3.rs b/solutions/06_move_semantics/move_semantics3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/06_move_semantics/move_semantics4.rs b/solutions/06_move_semantics/move_semantics4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/06_move_semantics/move_semantics5.rs b/solutions/06_move_semantics/move_semantics5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/06_move_semantics/move_semantics6.rs b/solutions/06_move_semantics/move_semantics6.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/06_move_semantics/move_semantics6.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/07_structs/structs1.rs b/solutions/07_structs/structs1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/07_structs/structs1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/07_structs/structs2.rs b/solutions/07_structs/structs2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/07_structs/structs2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/07_structs/structs3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/08_enums/enums1.rs b/solutions/08_enums/enums1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/08_enums/enums1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/08_enums/enums2.rs b/solutions/08_enums/enums2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/08_enums/enums2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/08_enums/enums3.rs b/solutions/08_enums/enums3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/08_enums/enums3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/09_strings/strings1.rs b/solutions/09_strings/strings1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/09_strings/strings1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/09_strings/strings2.rs b/solutions/09_strings/strings2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/09_strings/strings2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/09_strings/strings3.rs b/solutions/09_strings/strings3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/09_strings/strings3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/09_strings/strings4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/10_modules/modules1.rs b/solutions/10_modules/modules1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/10_modules/modules1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/10_modules/modules2.rs b/solutions/10_modules/modules2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/10_modules/modules2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/10_modules/modules3.rs b/solutions/10_modules/modules3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/10_modules/modules3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/11_hashmaps/hashmaps2.rs b/solutions/11_hashmaps/hashmaps2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/11_hashmaps/hashmaps2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/12_options/options1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/12_options/options2.rs b/solutions/12_options/options2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/12_options/options2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/12_options/options3.rs b/solutions/12_options/options3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/12_options/options3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/13_error_handling/errors1.rs b/solutions/13_error_handling/errors1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/13_error_handling/errors1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/13_error_handling/errors2.rs b/solutions/13_error_handling/errors2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/13_error_handling/errors2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/13_error_handling/errors3.rs b/solutions/13_error_handling/errors3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/13_error_handling/errors3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/13_error_handling/errors4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/13_error_handling/errors5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/13_error_handling/errors6.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/14_generics/generics1.rs b/solutions/14_generics/generics1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/14_generics/generics1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/14_generics/generics2.rs b/solutions/14_generics/generics2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/14_generics/generics2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/15_traits/traits1.rs b/solutions/15_traits/traits1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/15_traits/traits1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/15_traits/traits2.rs b/solutions/15_traits/traits2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/15_traits/traits2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/15_traits/traits3.rs b/solutions/15_traits/traits3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/15_traits/traits3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/15_traits/traits4.rs b/solutions/15_traits/traits4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/15_traits/traits4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/15_traits/traits5.rs b/solutions/15_traits/traits5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/15_traits/traits5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/16_lifetimes/lifetimes1.rs b/solutions/16_lifetimes/lifetimes1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/16_lifetimes/lifetimes1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/16_lifetimes/lifetimes3.rs b/solutions/16_lifetimes/lifetimes3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/16_lifetimes/lifetimes3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/17_tests/tests1.rs b/solutions/17_tests/tests1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/17_tests/tests1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/17_tests/tests2.rs b/solutions/17_tests/tests2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/17_tests/tests2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/17_tests/tests3.rs b/solutions/17_tests/tests3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/17_tests/tests3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/17_tests/tests4.rs b/solutions/17_tests/tests4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/17_tests/tests4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/18_iterators/iterators1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/18_iterators/iterators2.rs b/solutions/18_iterators/iterators2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/18_iterators/iterators2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/18_iterators/iterators3.rs b/solutions/18_iterators/iterators3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/18_iterators/iterators3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/18_iterators/iterators4.rs b/solutions/18_iterators/iterators4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/18_iterators/iterators4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/18_iterators/iterators5.rs b/solutions/18_iterators/iterators5.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/18_iterators/iterators5.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/19_smart_pointers/arc1.rs b/solutions/19_smart_pointers/arc1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/19_smart_pointers/arc1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/19_smart_pointers/box1.rs b/solutions/19_smart_pointers/box1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/19_smart_pointers/box1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/19_smart_pointers/cow1.rs b/solutions/19_smart_pointers/cow1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/19_smart_pointers/cow1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/rc1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/19_smart_pointers/rc1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/20_threads/threads1.rs b/solutions/20_threads/threads1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/20_threads/threads1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/20_threads/threads2.rs b/solutions/20_threads/threads2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/20_threads/threads2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/20_threads/threads3.rs b/solutions/20_threads/threads3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/20_threads/threads3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/21_macros/macros1.rs b/solutions/21_macros/macros1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/21_macros/macros1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/21_macros/macros2.rs b/solutions/21_macros/macros2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/21_macros/macros2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/21_macros/macros3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/21_macros/macros4.rs b/solutions/21_macros/macros4.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/21_macros/macros4.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/22_clippy/clippy1.rs b/solutions/22_clippy/clippy1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/22_clippy/clippy1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/22_clippy/clippy2.rs b/solutions/22_clippy/clippy2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/22_clippy/clippy2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/22_clippy/clippy3.rs b/solutions/22_clippy/clippy3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/22_clippy/clippy3.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/as_ref_mut.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/23_conversions/as_ref_mut.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/23_conversions/from_into.rs b/solutions/23_conversions/from_into.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/23_conversions/from_into.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/from_str.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/23_conversions/from_str.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/try_from_into.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/23_conversions/try_from_into.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/23_conversions/using_as.rs b/solutions/23_conversions/using_as.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/23_conversions/using_as.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/quizzes/quiz1.rs b/solutions/quizzes/quiz1.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/quizzes/quiz1.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/quizzes/quiz2.rs b/solutions/quizzes/quiz2.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/quizzes/quiz2.rs @@ -0,0 +1 @@ +// TODO diff --git a/solutions/quizzes/quiz3.rs b/solutions/quizzes/quiz3.rs new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/solutions/quizzes/quiz3.rs @@ -0,0 +1 @@ +// TODO diff --git a/src/app_state.rs b/src/app_state.rs index 09de2a3e..0767c2b2 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,11 +11,7 @@ use std::{ process::{Command, Stdio}, }; -use crate::{ - embedded::{WriteStrategy, EMBEDDED_FILES}, - exercise::Exercise, - info_file::ExerciseInfo, -}; +use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -100,10 +96,15 @@ impl AppState { exercise_info.name.shrink_to_fit(); let name = exercise_info.name.leak(); + let dir = exercise_info.dir.map(|mut dir| { + dir.shrink_to_fit(); + &*dir.leak() + }); let hint = exercise_info.hint.trim().to_owned(); Exercise { + dir, name, path, mode: exercise_info.mode, @@ -181,10 +182,16 @@ impl AppState { Ok(()) } - fn reset_path(&self, path: &str) -> Result<()> { + fn reset(&self, ind: usize, dir_name: Option<&str>, path: &str) -> Result<()> { if self.official_exercises { return EMBEDDED_FILES - .write_exercise_to_disk(path, WriteStrategy::Overwrite) + .write_exercise_to_disk( + ind, + dir_name.context( + "Official exercises must be nested in the `exercises` directory", + )?, + path, + ) .with_context(|| format!("Failed to reset the exercise {path}")); } @@ -209,11 +216,11 @@ impl AppState { } pub fn reset_current_exercise(&mut self) -> Result<&'static str> { - let path = self.current_exercise().path; self.set_pending(self.current_exercise_ind)?; - self.reset_path(path)?; + let exercise = self.current_exercise(); + self.reset(self.current_exercise_ind, exercise.dir, exercise.path)?; - Ok(path) + Ok(exercise.path) } pub fn reset_exercise_by_ind(&mut self, exercise_ind: usize) -> Result<&'static str> { @@ -221,11 +228,11 @@ impl AppState { bail!(BAD_INDEX_ERR); } - let path = self.exercises[exercise_ind].path; self.set_pending(exercise_ind)?; - self.reset_path(path)?; + let exercise = &self.exercises[exercise_ind]; + self.reset(exercise_ind, exercise.dir, exercise.path)?; - Ok(path) + Ok(exercise.path) } fn next_pending_exercise_ind(&self) -> Option { diff --git a/src/embedded.rs b/src/embedded.rs index eae30998..ccd8dca4 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,9 +1,10 @@ use std::{ - fs::{create_dir, File, OpenOptions}, + fs::{create_dir, OpenOptions}, io::{self, Write}, - path::Path, }; +use crate::info_file::ExerciseInfo; + pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); #[derive(Clone, Copy)] @@ -13,107 +14,78 @@ pub enum WriteStrategy { } impl WriteStrategy { - fn open>(self, path: P) -> io::Result { - match self { + fn write(self, path: &str, content: &[u8]) -> io::Result<()> { + let file = match self { Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), Self::Overwrite => OpenOptions::new() .create(true) .write(true) .truncate(true) .open(path), - } + }; + + file?.write_all(content) } } -struct EmbeddedFile { - path: &'static str, - content: &'static [u8], +struct ExerciseFiles { + exercise: &'static [u8], + solution: &'static [u8], } -impl EmbeddedFile { - fn write_to_disk(&self, strategy: WriteStrategy) -> io::Result<()> { - strategy.open(self.path)?.write_all(self.content) - } +struct ExerciseDir { + name: &'static str, + readme: &'static [u8], } -struct EmbeddedFlatDir { - path: &'static str, - readme: EmbeddedFile, - content: &'static [EmbeddedFile], -} - -impl EmbeddedFlatDir { +impl ExerciseDir { fn init_on_disk(&self) -> io::Result<()> { - let path = Path::new(self.path); - - if let Err(e) = create_dir(path) { - if e.kind() != io::ErrorKind::AlreadyExists { - return Err(e); + if let Err(e) = create_dir(format!("exercises/{}", self.name)) { + if e.kind() == io::ErrorKind::AlreadyExists { + return Ok(()); } + + return Err(e); } - self.readme.write_to_disk(WriteStrategy::Overwrite) + WriteStrategy::Overwrite.write(&format!("exercises/{}/README.md", self.name), self.readme) } } -struct ExercisesDir { - readme: EmbeddedFile, - files: &'static [EmbeddedFile], - dirs: &'static [EmbeddedFlatDir], -} - pub struct EmbeddedFiles { - exercises_dir: ExercisesDir, + exercise_files: &'static [ExerciseFiles], + exercise_dirs: &'static [ExerciseDir], } impl EmbeddedFiles { - pub fn init_exercises_dir(&self) -> io::Result<()> { + pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> io::Result<()> { create_dir("exercises")?; - self.exercises_dir - .readme - .write_to_disk(WriteStrategy::IfNotExists)?; + WriteStrategy::IfNotExists.write( + "exercises/README.md", + include_bytes!("../exercises/README.md"), + )?; - for file in self.exercises_dir.files { - file.write_to_disk(WriteStrategy::IfNotExists)?; + for dir in self.exercise_dirs { + dir.init_on_disk()?; } - for dir in self.exercises_dir.dirs { - dir.init_on_disk()?; - - for file in dir.content { - file.write_to_disk(WriteStrategy::IfNotExists)?; - } + for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { + WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?; } Ok(()) } - pub fn write_exercise_to_disk

(&self, path: P, strategy: WriteStrategy) -> io::Result<()> - where - P: AsRef, - { - let path = path.as_ref(); + pub fn write_exercise_to_disk(&self, exercise_ind: usize, dir_name: &str, path: &str) -> io::Result<()> { + let Some(dir) = self.exercise_dirs.iter().find(|dir| dir.name == dir_name) else { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("`{dir_name}` not found in the embedded directories"), + )); + }; - if let Some(file) = self - .exercises_dir - .files - .iter() - .find(|file| Path::new(file.path) == path) - { - return file.write_to_disk(strategy); - } - - for dir in self.exercises_dir.dirs { - if let Some(file) = dir.content.iter().find(|file| Path::new(file.path) == path) { - dir.init_on_disk()?; - return file.write_to_disk(strategy); - } - } - - Err(io::Error::new( - io::ErrorKind::NotFound, - format!("{} not found in the embedded files", path.display()), - )) + dir.init_on_disk()?; + WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].exercise) } } diff --git a/src/exercise.rs b/src/exercise.rs index e85efe4c..4493a575 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -31,6 +31,7 @@ impl<'a> Display for TerminalFileLink<'a> { } pub struct Exercise { + pub dir: Option<&'static str>, // Exercise's unique name pub name: &'static str, // Exercise's path diff --git a/src/init.rs b/src/init.rs index f210db77..f1a95099 100644 --- a/src/init.rs +++ b/src/init.rs @@ -24,11 +24,11 @@ pub fn init() -> Result<()> { set_current_dir("rustlings") .context("Failed to change the current directory to `rustlings`")?; + let info_file = InfoFile::parse()?; EMBEDDED_FILES - .init_exercises_dir() + .init_exercises_dir(&info_file.exercises) .context("Failed to initialize the `rustlings/exercises` directory")?; - let info_file = InfoFile::parse()?; let current_cargo_toml = include_str!("../dev/Cargo.toml"); // Skip the first line (comment). let newline_ind = current_cargo_toml From b77007887c5e3e369a3dc7347c93c6b7293f8534 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 00:47:46 +0200 Subject: [PATCH 202/433] Write the solution file on done --- src/app_state.rs | 10 +++++++++ src/embedded.rs | 54 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 0767c2b2..152a6744 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -264,6 +264,16 @@ impl AppState { self.n_done += 1; } + if self.official_exercises { + EMBEDDED_FILES.write_solution_to_disk( + self.current_exercise_ind, + exercise + .dir + .context("Official exercises must be nested in the `exercises` directory")?, + exercise.name, + )?; + } + let Some(ind) = self.next_pending_exercise_ind() else { writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; diff --git a/src/embedded.rs b/src/embedded.rs index ccd8dca4..2de3b1cf 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,5 +1,6 @@ +use anyhow::{bail, Context, Error, Result}; use std::{ - fs::{create_dir, OpenOptions}, + fs::{create_dir, create_dir_all, OpenOptions}, io::{self, Write}, }; @@ -14,7 +15,7 @@ pub enum WriteStrategy { } impl WriteStrategy { - fn write(self, path: &str, content: &[u8]) -> io::Result<()> { + fn write(self, path: &str, content: &[u8]) -> Result<()> { let file = match self { Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path), Self::Overwrite => OpenOptions::new() @@ -24,7 +25,9 @@ impl WriteStrategy { .open(path), }; - file?.write_all(content) + file.context("Failed to open the file `{path}` in write mode")? + .write_all(content) + .context("Failed to write the file {path}") } } @@ -39,16 +42,22 @@ struct ExerciseDir { } impl ExerciseDir { - fn init_on_disk(&self) -> io::Result<()> { - if let Err(e) = create_dir(format!("exercises/{}", self.name)) { + fn init_on_disk(&self) -> Result<()> { + let dir_path = format!("exercises/{}", self.name); + if let Err(e) = create_dir(&dir_path) { if e.kind() == io::ErrorKind::AlreadyExists { return Ok(()); } - return Err(e); + return Err( + Error::from(e).context(format!("Failed to create the directory {dir_path}")) + ); } - WriteStrategy::Overwrite.write(&format!("exercises/{}/README.md", self.name), self.readme) + WriteStrategy::Overwrite + .write(&format!("exercises/{}/README.md", self.name), self.readme)?; + + Ok(()) } } @@ -58,8 +67,8 @@ pub struct EmbeddedFiles { } impl EmbeddedFiles { - pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> io::Result<()> { - create_dir("exercises")?; + pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { + create_dir("exercises").context("Failed to create the directory `exercises`")?; WriteStrategy::IfNotExists.write( "exercises/README.md", @@ -77,15 +86,32 @@ impl EmbeddedFiles { Ok(()) } - pub fn write_exercise_to_disk(&self, exercise_ind: usize, dir_name: &str, path: &str) -> io::Result<()> { + pub fn write_exercise_to_disk( + &self, + exercise_ind: usize, + dir_name: &str, + path: &str, + ) -> Result<()> { let Some(dir) = self.exercise_dirs.iter().find(|dir| dir.name == dir_name) else { - return Err(io::Error::new( - io::ErrorKind::NotFound, - format!("`{dir_name}` not found in the embedded directories"), - )); + bail!("`{dir_name}` not found in the embedded directories"); }; dir.init_on_disk()?; WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].exercise) } + + pub fn write_solution_to_disk( + &self, + exercise_ind: usize, + dir_name: &str, + exercise_name: &str, + ) -> Result<()> { + let dir_path = format!("solutions/{dir_name}"); + create_dir_all(&dir_path).context("Failed to create the directory {dir_path}")?; + + WriteStrategy::Overwrite.write( + &format!("{dir_path}/{exercise_name}.rs"), + self.exercise_files[exercise_ind].solution, + ) + } } From e4ee2cd548ded403dd540ab21f97093633a72da6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 00:48:58 +0200 Subject: [PATCH 203/433] Don't write solutions in debug mode --- src/app_state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 152a6744..6f393bc5 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,7 +11,7 @@ use std::{ process::{Command, Stdio}, }; -use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo}; +use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo, DEBUG_PROFILE}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -264,7 +264,7 @@ impl AppState { self.n_done += 1; } - if self.official_exercises { + if self.official_exercises && !DEBUG_PROFILE { EMBEDDED_FILES.write_solution_to_disk( self.current_exercise_ind, exercise From ef02c6c6ab93adf64353c13d6f036d1cd4187af0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 00:58:52 +0200 Subject: [PATCH 204/433] Use the embedded info.toml in debug mode --- src/info_file.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/info_file.rs b/src/info_file.rs index 879609e3..f344464b 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; -use std::fs; +use std::{fs, io::ErrorKind}; + +use crate::DEBUG_PROFILE; // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] @@ -46,18 +48,27 @@ pub struct InfoFile { } impl InfoFile { + fn from_embedded() -> Result { + toml_edit::de::from_str(include_str!("../info.toml")) + .context("Failed to parse the embedded `info.toml` file") + } + pub fn parse() -> Result { + if DEBUG_PROFILE { + return Self::from_embedded(); + } + // Read a local `info.toml` if it exists. - let slf: Self = match fs::read_to_string("info.toml") { - Ok(file_content) => toml_edit::de::from_str(&file_content) + let slf = match fs::read_to_string("info.toml") { + Ok(file_content) => toml_edit::de::from_str::(&file_content) .context("Failed to parse the `info.toml` file")?, - Err(e) => match e.kind() { - std::io::ErrorKind::NotFound => { - toml_edit::de::from_str(include_str!("../info.toml")) - .context("Failed to parse the embedded `info.toml` file")? + Err(e) => { + if e.kind() == ErrorKind::NotFound { + return Self::from_embedded(); } - _ => return Err(Error::from(e).context("Failed to read the `info.toml` file")), - }, + + return Err(Error::from(e).context("Failed to read the `info.toml` file")); + } }; if slf.exercises.is_empty() { From edf57626129467dacc0c6f04b2ca00e64d5b2245 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 01:17:39 +0200 Subject: [PATCH 205/433] Preallocate path --- src/embedded.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 2de3b1cf..756b4141 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -43,7 +43,13 @@ struct ExerciseDir { impl ExerciseDir { fn init_on_disk(&self) -> Result<()> { - let dir_path = format!("exercises/{}", self.name); + let path_prefix = "exercises/"; + let readme_path_postfix = "/README.md"; + let mut dir_path = + String::with_capacity(path_prefix.len() + self.name.len() + readme_path_postfix.len()); + dir_path.push_str(path_prefix); + dir_path.push_str(self.name); + if let Err(e) = create_dir(&dir_path) { if e.kind() == io::ErrorKind::AlreadyExists { return Ok(()); @@ -54,8 +60,11 @@ impl ExerciseDir { ); } - WriteStrategy::Overwrite - .write(&format!("exercises/{}/README.md", self.name), self.readme)?; + let readme_path = { + dir_path.push_str(readme_path_postfix); + dir_path + }; + WriteStrategy::Overwrite.write(&readme_path, self.readme)?; Ok(()) } From 8a085a0a85c759029cd57c28364867bde817e738 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 02:52:30 +0200 Subject: [PATCH 206/433] Dump solution and show its path --- src/app_state.rs | 45 ++++++++++++++++++++++++++++++++++---------- src/embedded.rs | 10 ++++------ src/exercise.rs | 28 ++------------------------- src/main.rs | 1 + src/run.rs | 14 ++++++++++++-- src/terminal_link.rs | 23 ++++++++++++++++++++++ src/watch/state.rs | 35 ++++++++++++++++++++++++++-------- 7 files changed, 104 insertions(+), 52 deletions(-) create mode 100644 src/terminal_link.rs diff --git a/src/app_state.rs b/src/app_state.rs index 6f393bc5..33d3de2b 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -257,6 +257,41 @@ impl AppState { } } + pub fn current_solution_path(&self) -> Result> { + if DEBUG_PROFILE { + return Ok(None); + } + + let current_exercise = self.current_exercise(); + + if self.official_exercises { + let dir_name = current_exercise + .dir + .context("Official exercises must be nested in the `exercises` directory")?; + let solution_path = format!("solutions/{dir_name}/{}.rs", current_exercise.name); + + EMBEDDED_FILES.write_solution_to_disk( + self.current_exercise_ind, + dir_name, + &solution_path, + )?; + + Ok(Some(solution_path)) + } else { + let solution_path = if let Some(dir) = current_exercise.dir { + format!("solutions/{dir}/{}.rs", current_exercise.name) + } else { + format!("solutions/{}.rs", current_exercise.name) + }; + + if Path::new(&solution_path).exists() { + return Ok(Some(solution_path)); + } + + Ok(None) + } + } + pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { @@ -264,16 +299,6 @@ impl AppState { self.n_done += 1; } - if self.official_exercises && !DEBUG_PROFILE { - EMBEDDED_FILES.write_solution_to_disk( - self.current_exercise_ind, - exercise - .dir - .context("Official exercises must be nested in the `exercises` directory")?, - exercise.name, - )?; - } - let Some(ind) = self.next_pending_exercise_ind() else { writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; diff --git a/src/embedded.rs b/src/embedded.rs index 756b4141..d7952a1f 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -113,14 +113,12 @@ impl EmbeddedFiles { &self, exercise_ind: usize, dir_name: &str, - exercise_name: &str, + path: &str, ) -> Result<()> { let dir_path = format!("solutions/{dir_name}"); - create_dir_all(&dir_path).context("Failed to create the directory {dir_path}")?; + create_dir_all(&dir_path) + .with_context(|| format!("Failed to create the directory {dir_path}"))?; - WriteStrategy::Overwrite.write( - &format!("{dir_path}/{exercise_name}.rs"), - self.exercise_files[exercise_ind].solution, - ) + WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].solution) } } diff --git a/src/exercise.rs b/src/exercise.rs index 4493a575..45ac2086 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -2,33 +2,11 @@ use anyhow::{Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - fs, path::Path, process::{Command, Output}, }; -use crate::{info_file::Mode, DEBUG_PROFILE}; - -pub struct TerminalFileLink<'a> { - path: &'a str, -} - -impl<'a> Display for TerminalFileLink<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Ok(Some(canonical_path)) = fs::canonicalize(self.path) - .as_deref() - .map(|path| path.to_str()) - { - write!( - f, - "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", - canonical_path, self.path, - ) - } else { - write!(f, "{}", self.path,) - } - } -} +use crate::{info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; pub struct Exercise { pub dir: Option<&'static str>, @@ -85,9 +63,7 @@ impl Exercise { } pub fn terminal_link(&self) -> StyledContent> { - style(TerminalFileLink { path: self.path }) - .underlined() - .blue() + style(TerminalFileLink(self.path)).underlined().blue() } } diff --git a/src/main.rs b/src/main.rs index 9ff218a3..790fff69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod init; mod list; mod progress_bar; mod run; +mod terminal_link; mod watch; const CURRENT_FORMAT_VERSION: u8 = 1; diff --git a/src/run.rs b/src/run.rs index 863b584e..a2b69720 100644 --- a/src/run.rs +++ b/src/run.rs @@ -1,8 +1,11 @@ use anyhow::{bail, Result}; -use crossterm::style::Stylize; +use crossterm::style::{style, Stylize}; use std::io::{self, Write}; -use crate::app_state::{AppState, ExercisesProgress}; +use crate::{ + app_state::{AppState, ExercisesProgress}, + terminal_link::TerminalFileLink, +}; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); @@ -29,6 +32,13 @@ pub fn run(app_state: &mut AppState) -> Result<()> { exercise.path.green(), ))?; + if let Some(solution_path) = app_state.current_solution_path()? { + println!( + "\nA solution file can be found at {}\n", + style(TerminalFileLink(&solution_path)).underlined().green(), + ); + } + match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), ExercisesProgress::Pending => println!( diff --git a/src/terminal_link.rs b/src/terminal_link.rs new file mode 100644 index 00000000..c9e6bced --- /dev/null +++ b/src/terminal_link.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{self, Display, Formatter}, + fs, +}; + +pub struct TerminalFileLink<'a>(pub &'a str); + +impl<'a> Display for TerminalFileLink<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Ok(Some(canonical_path)) = fs::canonicalize(self.0) + .as_deref() + .map(|path| path.to_str()) + { + write!( + f, + "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", + canonical_path, self.0, + ) + } else { + write!(f, "{}", self.0) + } + } +} diff --git a/src/watch/state.rs b/src/watch/state.rs index c0f6c532..5f4abf3c 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,6 +1,6 @@ use anyhow::Result; use crossterm::{ - style::Stylize, + style::{style, Stylize}, terminal::{size, Clear, ClearType}, ExecutableCommand, }; @@ -9,15 +9,22 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, progress_bar::progress_bar, + terminal_link::TerminalFileLink, }; +enum DoneStatus { + DoneWithSolution(String), + DoneWithoutSolution, + Pending, +} + pub struct WatchState<'a> { writer: StdoutLock<'a>, app_state: &'a mut AppState, stdout: Option>, stderr: Option>, show_hint: bool, - show_done: bool, + done_status: DoneStatus, manual_run: bool, } @@ -31,7 +38,7 @@ impl<'a> WatchState<'a> { stdout: None, stderr: None, show_hint: false, - show_done: false, + done_status: DoneStatus::Pending, manual_run, } } @@ -49,13 +56,18 @@ impl<'a> WatchState<'a> { if output.status.success() { self.stderr = None; - self.show_done = true; + self.done_status = + if let Some(solution_path) = self.app_state.current_solution_path()? { + DoneStatus::DoneWithSolution(solution_path) + } else { + DoneStatus::DoneWithoutSolution + }; } else { self.app_state .set_pending(self.app_state.current_exercise_ind())?; self.stderr = Some(output.stderr); - self.show_done = false; + self.done_status = DoneStatus::Pending; } self.render() @@ -67,7 +79,7 @@ impl<'a> WatchState<'a> { } pub fn next_exercise(&mut self) -> Result { - if !self.show_done { + if matches!(self.done_status, DoneStatus::Pending) { self.writer .write_all(b"The current exercise isn't done yet\n")?; self.show_prompt()?; @@ -84,7 +96,7 @@ impl<'a> WatchState<'a> { self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; } - if self.show_done { + if !matches!(self.done_status, DoneStatus::Pending) { self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; } @@ -124,7 +136,7 @@ impl<'a> WatchState<'a> { ))?; } - if self.show_done { + if !matches!(self.done_status, DoneStatus::Pending) { self.writer.write_fmt(format_args!( "{}\n\n", "Exercise done ✓ @@ -134,6 +146,13 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise ))?; } + if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { + self.writer.write_fmt(format_args!( + "A solution file can be found at {}\n\n", + style(TerminalFileLink(solution_path)).underlined().green() + ))?; + } + let line_width = size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), From 4ef345e706ab5150d96faadf41a28aee49534361 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 15:58:34 +0200 Subject: [PATCH 207/433] Update dependency --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93617fab..df9ee743 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,11 +954,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "134306a13c5647ad6453e8deaec55d3a44d6021970129e6188735e74bf546697" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] From 8ebd2f9df232dbe630510b1994c896871aa58208 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 16:15:14 +0200 Subject: [PATCH 208/433] Update Cargo.toml files --- Cargo.toml | 13 ++++++++++--- rustlings-macros/Cargo.toml | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e6a24ca..f4615b13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,14 @@ exclude = [ ] [workspace.package] -version = "6.0.0-alpha.0" +version = "6.0.0-beta.0" authors = [ "Liv ", + "Mo Bitar ", + # Alumni "Carol (Nichols || Goulding) ", - "Mo ", ] +repository = "https://github.com/rust-lang/rustlings" license = "MIT" edition = "2021" @@ -27,8 +29,13 @@ description = "Small exercises to get you used to reading and writing Rust code! default-run = "rustlings" version.workspace = true authors.workspace = true +repository.workspace = true license.workspace = true edition.workspace = true +keywords = [ + "exercise", + "learning", +] include = [ "/exercises/", "/info.toml", @@ -44,7 +51,7 @@ crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" } +rustlings-macros = { path = "rustlings-macros", version = "6.0.0-beta.0" } serde.workspace = true toml_edit.workspace = true which = "6.0.1" diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index f925f692..c9c1d2f7 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "rustlings-macros" -description = "A macros crate intended to be only used by rustlings" +description = "A macros crate intended to be used only by Rustlings" version.workspace = true authors.workspace = true +repository.workspace = true license.workspace = true edition.workspace = true From 0df0be835279a9dd176244b9014f459399153300 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 16:26:34 +0200 Subject: [PATCH 209/433] Update Cargo.lock --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df9ee743..5f0b5978 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,7 +669,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.0.0-alpha.0" +version = "6.0.0-beta.0" dependencies = [ "anyhow", "assert_cmd", @@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-alpha.0" +version = "6.0.0-beta.0" dependencies = [ "quote", "serde", From d8c2ab8349854cbc7f4a994c7413d266cc38bc24 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 24 Apr 2024 16:26:48 +0200 Subject: [PATCH 210/433] Fix tests --- src/info_file.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/info_file.rs b/src/info_file.rs index f344464b..6938cd03 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,8 +2,6 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; use std::{fs, io::ErrorKind}; -use crate::DEBUG_PROFILE; - // The mode of the exercise. #[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] @@ -48,23 +46,15 @@ pub struct InfoFile { } impl InfoFile { - fn from_embedded() -> Result { - toml_edit::de::from_str(include_str!("../info.toml")) - .context("Failed to parse the embedded `info.toml` file") - } - pub fn parse() -> Result { - if DEBUG_PROFILE { - return Self::from_embedded(); - } - // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { Ok(file_content) => toml_edit::de::from_str::(&file_content) .context("Failed to parse the `info.toml` file")?, Err(e) => { if e.kind() == ErrorKind::NotFound { - return Self::from_embedded(); + return toml_edit::de::from_str(include_str!("../info.toml")) + .context("Failed to parse the embedded `info.toml` file"); } return Err(Error::from(e).context("Failed to read the `info.toml` file")); From 67fa01774223b08833c21baeb13bdec9e4a298a0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 01:56:01 +0200 Subject: [PATCH 211/433] Use os_pipe --- Cargo.lock | 11 +++++ Cargo.toml | 1 + src/app_state.rs | 13 +++++- src/exercise.rs | 110 +++++++++++++++++++++++++++++++++++++++------ src/main.rs | 6 ++- src/run.rs | 11 +++-- src/watch/state.rs | 26 +++-------- 7 files changed, 135 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f0b5978..823fec36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -519,6 +519,16 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -677,6 +687,7 @@ dependencies = [ "crossterm", "hashbrown", "notify-debouncer-mini", + "os_pipe", "predicates", "ratatui", "rustlings-macros", diff --git a/Cargo.toml b/Cargo.toml index f4615b13..31e74568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" +os_pipe = "1.1.5" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "6.0.0-beta.0" } serde.workspace = true diff --git a/src/app_state.rs b/src/app_state.rs index 33d3de2b..4160f6e5 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,7 +11,12 @@ use std::{ process::{Command, Stdio}, }; -use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo, DEBUG_PROFILE}; +use crate::{ + embedded::EMBEDDED_FILES, + exercise::{Exercise, OUTPUT_CAPACITY}, + info_file::ExerciseInfo, + DEBUG_PROFILE, +}; const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; @@ -302,11 +307,13 @@ impl AppState { let Some(ind) = self.next_pending_exercise_ind() else { writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); for (exercise_ind, exercise) in self.exercises().iter().enumerate() { writer.write_fmt(format_args!("Running {exercise} ... "))?; writer.flush()?; - if !exercise.run()?.status.success() { + let success = exercise.run(&mut output)?; + if !success { writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; self.current_exercise_ind = exercise_ind; @@ -322,6 +329,8 @@ impl AppState { } writer.write_fmt(format_args!("{}\n", "ok".green()))?; + + output.clear(); } writer.execute(Clear(ClearType::All))?; diff --git a/src/exercise.rs b/src/exercise.rs index 45ac2086..17cc8d77 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -2,11 +2,42 @@ use anyhow::{Context, Result}; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - path::Path, - process::{Command, Output}, + io::{Read, Write}, + process::Command, }; -use crate::{info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; +use crate::{in_official_repo, info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; + +// TODO +pub const OUTPUT_CAPACITY: usize = 1 << 12; + +fn run_command(mut cmd: Command, cmd_description: &str, output: &mut Vec) -> Result { + let (mut reader, writer) = os_pipe::pipe().with_context(|| { + format!("Failed to create a pipe to run the command `{cmd_description}``") + })?; + + let mut handle = cmd + .stdout(writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{cmd_description}`") + })?) + .stderr(writer) + .spawn() + .with_context(|| format!("Failed to run the command `{cmd_description}`"))?; + + // Prevent pipe deadlock. + drop(cmd); + + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{cmd_description}`"))?; + + output.push(b'\n'); + + handle + .wait() + .with_context(|| format!("Failed to wait on the command `{cmd_description}` to exit")) + .map(|status| status.success()) +} pub struct Exercise { pub dir: Option<&'static str>, @@ -22,13 +53,30 @@ pub struct Exercise { } impl Exercise { - fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { + fn run_bin(&self, output: &mut Vec) -> Result { + writeln!(output, "{}", "Output".bold().magenta().underlined())?; + + let bin_path = format!("target/debug/{}", self.name); + run_command(Command::new(&bin_path), &bin_path, output) + } + + fn cargo_cmd( + &self, + command: &str, + args: &[&str], + cmd_description: &str, + output: &mut Vec, + dev: bool, + ) -> Result { let mut cmd = Command::new("cargo"); cmd.arg(command); // A hack to make `cargo run` work when developing Rustlings. - if DEBUG_PROFILE && Path::new("tests").exists() { - cmd.arg("--manifest-path").arg("dev/Cargo.toml"); + if dev { + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg("target"); } cmd.arg("--color") @@ -36,15 +84,43 @@ impl Exercise { .arg("-q") .arg("--bin") .arg(self.name) - .args(args) - .output() - .context("Failed to run Cargo") + .args(args); + + run_command(cmd, cmd_description, output) } - pub fn run(&self) -> Result { + fn cargo_cmd_with_bin_output( + &self, + command: &str, + args: &[&str], + cmd_description: &str, + output: &mut Vec, + dev: bool, + ) -> Result { + // Discard the output of `cargo build` because it will be shown again by the Cargo command. + output.clear(); + + let cargo_cmd_success = self.cargo_cmd(command, args, cmd_description, output, dev)?; + + let run_success = self.run_bin(output)?; + + Ok(cargo_cmd_success && run_success) + } + + pub fn run(&self, output: &mut Vec) -> Result { + output.clear(); + + // Developing the official Rustlings. + let dev = DEBUG_PROFILE && in_official_repo(); + + let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev)?; + if !build_success { + return Ok(false); + } + match self.mode { - Mode::Run => self.cargo_cmd("run", &[]), - Mode::Test => self.cargo_cmd( + Mode::Run => self.run_bin(output), + Mode::Test => self.cargo_cmd_with_bin_output( "test", &[ "--", @@ -54,10 +130,16 @@ impl Exercise { "--format", "pretty", ], + "cargo test …", + output, + dev, ), - Mode::Clippy => self.cargo_cmd( + Mode::Clippy => self.cargo_cmd_with_bin_output( "clippy", - &["--", "-D", "warnings", "-D", "clippy::float_cmp"], + &["--", "-D", "warnings"], + "cargo clippy …", + output, + dev, ), } } diff --git a/src/main.rs b/src/main.rs index 790fff69..a9285044 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,10 +75,14 @@ enum Subcommands { Dev(DevCommands), } +fn in_official_repo() -> bool { + Path::new("dev/rustlings-repo.txt").exists() +} + fn main() -> Result<()> { let args = Args::parse(); - if !DEBUG_PROFILE && Path::new("dev/rustlings-repo.txt").exists() { + if !DEBUG_PROFILE && in_official_repo() { bail!("{OLD_METHOD_ERR}"); } diff --git a/src/run.rs b/src/run.rs index a2b69720..1db8dcbb 100644 --- a/src/run.rs +++ b/src/run.rs @@ -4,20 +4,19 @@ use std::io::{self, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, + exercise::OUTPUT_CAPACITY, terminal_link::TerminalFileLink, }; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); - let output = exercise.run()?; + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + let success = exercise.run(&mut output)?; let mut stdout = io::stdout().lock(); - stdout.write_all(&output.stdout)?; - stdout.write_all(b"\n")?; - stdout.write_all(&output.stderr)?; - stdout.flush()?; + stdout.write_all(&output)?; - if !output.status.success() { + if !success { app_state.set_pending(app_state.current_exercise_ind())?; bail!( diff --git a/src/watch/state.rs b/src/watch/state.rs index 5f4abf3c..df492dc8 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -8,6 +8,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, + exercise::OUTPUT_CAPACITY, progress_bar::progress_bar, terminal_link::TerminalFileLink, }; @@ -21,8 +22,7 @@ enum DoneStatus { pub struct WatchState<'a> { writer: StdoutLock<'a>, app_state: &'a mut AppState, - stdout: Option>, - stderr: Option>, + output: Vec, show_hint: bool, done_status: DoneStatus, manual_run: bool, @@ -35,8 +35,7 @@ impl<'a> WatchState<'a> { Self { writer, app_state, - stdout: None, - stderr: None, + output: Vec::with_capacity(OUTPUT_CAPACITY), show_hint: false, done_status: DoneStatus::Pending, manual_run, @@ -51,11 +50,8 @@ impl<'a> WatchState<'a> { pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; - let output = self.app_state.current_exercise().run()?; - self.stdout = Some(output.stdout); - - if output.status.success() { - self.stderr = None; + let success = self.app_state.current_exercise().run(&mut self.output)?; + if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { DoneStatus::DoneWithSolution(solution_path) @@ -66,7 +62,6 @@ impl<'a> WatchState<'a> { self.app_state .set_pending(self.app_state.current_exercise_ind())?; - self.stderr = Some(output.stderr); self.done_status = DoneStatus::Pending; } @@ -116,16 +111,7 @@ impl<'a> WatchState<'a> { self.writer.execute(Clear(ClearType::All))?; - if let Some(stdout) = &self.stdout { - self.writer.write_all(stdout)?; - self.writer.write_all(b"\n")?; - } - - if let Some(stderr) = &self.stderr { - self.writer.write_all(stderr)?; - self.writer.write_all(b"\n")?; - } - + self.writer.write_all(&self.output)?; self.writer.write_all(b"\n")?; if self.show_hint { From f92d45fa685e308c009cdf09d341bda41fcf9c52 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 02:03:26 +0200 Subject: [PATCH 212/433] Use write macros instead of write_fmt --- src/app_state.rs | 6 +++--- src/list/state.rs | 3 +-- src/run.rs | 7 ++++--- src/watch/state.rs | 37 ++++++++++++++++++++----------------- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 4160f6e5..11ac8ee4 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -309,12 +309,12 @@ impl AppState { let mut output = Vec::with_capacity(OUTPUT_CAPACITY); for (exercise_ind, exercise) in self.exercises().iter().enumerate() { - writer.write_fmt(format_args!("Running {exercise} ... "))?; + write!(writer, "Running {exercise} ... ")?; writer.flush()?; let success = exercise.run(&mut output)?; if !success { - writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; + writeln!(writer, "{}\n", "FAILED".red())?; self.current_exercise_ind = exercise_ind; @@ -328,7 +328,7 @@ impl AppState { return Ok(ExercisesProgress::Pending); } - writer.write_fmt(format_args!("{}\n", "ok".green()))?; + writeln!(writer, "{}", "ok".green())?; output.clear(); } diff --git a/src/list/state.rs b/src/list/state.rs index 0253bb97..19a77fe0 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -231,8 +231,7 @@ impl<'a> UiState<'a> { .context("Invalid selection index")?; let exercise_path = self.app_state.reset_exercise_by_ind(ind)?; - self.message - .write_fmt(format_args!("The exercise {exercise_path} has been reset"))?; + write!(self.message, "The exercise {exercise_path} has been reset")?; Ok(self.with_updated_rows()) } diff --git a/src/run.rs b/src/run.rs index 1db8dcbb..cbc9ad70 100644 --- a/src/run.rs +++ b/src/run.rs @@ -25,11 +25,12 @@ pub fn run(app_state: &mut AppState) -> Result<()> { ); } - stdout.write_fmt(format_args!( - "{}{}\n", + writeln!( + stdout, + "{}{}", "✓ Successfully ran ".green(), exercise.path.green(), - ))?; + )?; if let Some(solution_path) = app_state.current_solution_path()? { println!( diff --git a/src/watch/state.rs b/src/watch/state.rs index df492dc8..40c01bfc 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -88,19 +88,18 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.manual_run { - self.writer.write_fmt(format_args!("{}un/", 'r'.bold()))?; + write!(self.writer, "{}un/", 'r'.bold())?; } if !matches!(self.done_status, DoneStatus::Pending) { - self.writer.write_fmt(format_args!("{}ext/", 'n'.bold()))?; + write!(self.writer, "{}ext/", 'n'.bold())?; } if !self.show_hint { - self.writer.write_fmt(format_args!("{}int/", 'h'.bold()))?; + write!(self.writer, "{}int/", 'h'.bold())?; } - self.writer - .write_fmt(format_args!("{}ist/{}uit? ", 'l'.bold(), 'q'.bold()))?; + write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?; self.writer.flush() } @@ -115,28 +114,31 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.show_hint { - self.writer.write_fmt(format_args!( - "{}\n{}\n\n", + writeln!( + self.writer, + "{}\n{}\n", "Hint".bold().cyan().underlined(), self.app_state.current_exercise().hint, - ))?; + )?; } if !matches!(self.done_status, DoneStatus::Pending) { - self.writer.write_fmt(format_args!( - "{}\n\n", + writeln!( + self.writer, + "{}\n", "Exercise done ✓ When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀" .bold() .green(), - ))?; + )?; } if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - self.writer.write_fmt(format_args!( - "A solution file can be found at {}\n\n", + writeln!( + self.writer, + "A solution file can be found at {}\n", style(TerminalFileLink(solution_path)).underlined().green() - ))?; + )?; } let line_width = size()?.0; @@ -145,10 +147,11 @@ When you are done experimenting, enter `n` or `next` to go to the next exercise self.app_state.exercises().len() as u16, line_width, )?; - self.writer.write_fmt(format_args!( - "{progress_bar}Current exercise: {}\n", + writeln!( + self.writer, + "{progress_bar}Current exercise: {}", self.app_state.current_exercise().terminal_link(), - ))?; + )?; self.show_prompt()?; From 2af0cd9ccef07edf27abf7046dbe15e32d1b476d Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 03:25:45 +0200 Subject: [PATCH 213/433] Replace `mode` by `test` and `strict_clippy` --- info.toml | 143 +++++++++++--------------------- src/app_state.rs | 5 +- src/dev/new.rs | 13 ++- src/exercise.rs | 131 +++++++++++++++++------------ src/info_file.rs | 22 ++--- tests/fixture/failure/info.toml | 3 +- tests/fixture/state/info.toml | 5 +- tests/fixture/success/info.toml | 3 +- 8 files changed, 147 insertions(+), 178 deletions(-) diff --git a/info.toml b/info.toml index d5369d50..14944728 100644 --- a/info.toml +++ b/info.toml @@ -39,7 +39,7 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md [[exercises]] name = "intro1" dir = "00_intro" -mode = "run" +test = false # TODO: Fix hint hint = """ Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file @@ -48,7 +48,7 @@ to move on to the next exercise.""" [[exercises]] name = "intro2" dir = "00_intro" -mode = "run" +test = false hint = """ The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" @@ -57,7 +57,7 @@ The compiler is informing us that we've got the name of the print macro wrong, a [[exercises]] name = "variables1" dir = "01_variables" -mode = "run" +test = false hint = """ The declaration in the first line in the main function is missing a keyword that is needed in Rust to create a new variable binding.""" @@ -65,7 +65,7 @@ that is needed in Rust to create a new variable binding.""" [[exercises]] name = "variables2" dir = "01_variables" -mode = "run" +test = false hint = """ The compiler message is saying that Rust cannot infer the type that the variable binding `x` has with what is given here. @@ -84,7 +84,7 @@ What if `x` is the same type as `10`? What if it's a different type?""" [[exercises]] name = "variables3" dir = "01_variables" -mode = "run" +test = false hint = """ Oops! In this exercise, we have a variable binding that we've created on in the first line in the `main` function, and we're trying to use it in the next line, @@ -98,7 +98,7 @@ programming language -- thankfully the Rust compiler has caught this for us!""" [[exercises]] name = "variables4" dir = "01_variables" -mode = "run" +test = false hint = """ In Rust, variable bindings are immutable by default. But here we're trying to reassign a different value to `x`! There's a keyword we can use to make @@ -107,7 +107,7 @@ a variable binding mutable instead.""" [[exercises]] name = "variables5" dir = "01_variables" -mode = "run" +test = false hint = """ In `variables4` we already learned how to make an immutable variable mutable using a special keyword. Unfortunately this doesn't help us much in this @@ -125,7 +125,7 @@ Try to solve this exercise afterwards using this technique.""" [[exercises]] name = "variables6" dir = "01_variables" -mode = "run" +test = false hint = """ We know about variables and mutability, but there is another important type of variable available: constants. @@ -145,7 +145,7 @@ https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants [[exercises]] name = "functions1" dir = "02_functions" -mode = "run" +test = false hint = """ This main function is calling a function that it expects to exist, but the function doesn't exist. It expects this function to have the name `call_me`. @@ -155,7 +155,7 @@ Sounds a lot like `main`, doesn't it?""" [[exercises]] name = "functions2" dir = "02_functions" -mode = "run" +test = false hint = """ Rust requires that all parts of a function's signature have type annotations, but `call_me` is missing the type annotation of `num`.""" @@ -163,7 +163,7 @@ but `call_me` is missing the type annotation of `num`.""" [[exercises]] name = "functions3" dir = "02_functions" -mode = "run" +test = false hint = """ This time, the function *declaration* is okay, but there's something wrong with the place where we're calling the function.""" @@ -171,7 +171,7 @@ with the place where we're calling the function.""" [[exercises]] name = "functions4" dir = "02_functions" -mode = "run" +test = false hint = """ The error message points to the function `sale_price` and says it expects a type after the `->`. This is where the function's return type should be -- take a @@ -180,7 +180,7 @@ look at the `is_even` function for an example!""" [[exercises]] name = "functions5" dir = "02_functions" -mode = "run" +test = false hint = """ This is a really common error that can be fixed by removing one character. It happens because Rust distinguishes between expressions and statements: @@ -199,7 +199,6 @@ They are not the same. There are two solutions: [[exercises]] name = "if1" dir = "03_if" -mode = "test" hint = """ It's possible to do this in one line if you would like! @@ -215,7 +214,6 @@ Remember in Rust that: [[exercises]] name = "if2" dir = "03_if" -mode = "test" hint = """ For that first compiler error, it's important in Rust that each conditional block returns the same type! To get the tests passing, you will need a couple @@ -224,7 +222,6 @@ conditions checking different input values.""" [[exercises]] name = "if3" dir = "03_if" -mode = "test" hint = """ In Rust, every arm of an `if` expression has to return the same type of value. Make sure the type is consistent across all arms.""" @@ -234,7 +231,6 @@ Make sure the type is consistent across all arms.""" [[exercises]] name = "quiz1" dir = "quizzes" -mode = "test" hint = "No hints this time ;)" # PRIMITIVE TYPES @@ -242,19 +238,19 @@ hint = "No hints this time ;)" [[exercises]] name = "primitive_types1" dir = "04_primitive_types" -mode = "run" +test = false hint = "No hints this time ;)" [[exercises]] name = "primitive_types2" dir = "04_primitive_types" -mode = "run" +test = false hint = "No hints this time ;)" [[exercises]] name = "primitive_types3" dir = "04_primitive_types" -mode = "run" +test = false hint = """ There's a shorthand to initialize Arrays with a certain size that does not require you to type in 100 items (but you certainly can if you want!). @@ -270,7 +266,6 @@ for `a.len() >= 100`?""" [[exercises]] name = "primitive_types4" dir = "04_primitive_types" -mode = "test" hint = """ Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use the @@ -285,7 +280,7 @@ https://doc.rust-lang.org/nomicon/coercions.html""" [[exercises]] name = "primitive_types5" dir = "04_primitive_types" -mode = "run" +test = false hint = """ Take a look at the 'Data Types -> The Tuple Type' section of the book: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type @@ -298,7 +293,6 @@ of the tuple. You can do it!!""" [[exercises]] name = "primitive_types6" dir = "04_primitive_types" -mode = "test" hint = """ While you could use a destructuring `let` for the tuple here, try indexing into it instead, as explained in the last example of the @@ -311,7 +305,6 @@ Now you have another tool in your toolbox!""" [[exercises]] name = "vecs1" dir = "05_vecs" -mode = "test" hint = """ In Rust, there are two ways to define a Vector. 1. One way is to use the `Vec::new()` function to create a new vector @@ -326,7 +319,6 @@ of the Rust book to learn more. [[exercises]] name = "vecs2" dir = "05_vecs" -mode = "test" hint = """ In the first function we are looping over the Vector and getting a reference to one `element` at a time. @@ -349,7 +341,6 @@ What do you think is the more commonly used pattern under Rust developers? [[exercises]] name = "move_semantics1" dir = "06_move_semantics" -mode = "test" hint = """ So you've got the "cannot borrow immutable local variable `vec` as mutable" error on the line where we push an element to the vector, right? @@ -363,7 +354,6 @@ happens!""" [[exercises]] name = "move_semantics2" dir = "06_move_semantics" -mode = "test" hint = """ When running this exercise for the first time, you'll notice an error about "borrow of moved value". In Rust, when an argument is passed to a function and @@ -384,7 +374,6 @@ try them all: [[exercises]] name = "move_semantics3" dir = "06_move_semantics" -mode = "test" hint = """ The difference between this one and the previous ones is that the first line of `fn fill_vec` that had `let mut vec = vec;` is no longer there. You can, @@ -394,7 +383,6 @@ an existing binding to be a mutable binding instead of an immutable one :)""" [[exercises]] name = "move_semantics4" dir = "06_move_semantics" -mode = "test" hint = """ Stop reading whenever you feel like you have enough direction :) Or try doing one step and then fixing the compiler errors that result! @@ -408,7 +396,6 @@ So the end goal is to: [[exercises]] name = "move_semantics5" dir = "06_move_semantics" -mode = "test" hint = """ Carefully reason about the range in which each mutable reference is in scope. Does it help to update the value of referent (`x`) immediately after @@ -420,7 +407,7 @@ https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-ref [[exercises]] name = "move_semantics6" dir = "06_move_semantics" -mode = "run" +test = false hint = """ To find the answer, you can consult the book section "References and Borrowing": https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html @@ -441,7 +428,6 @@ Another hint: it has to do with the `&` character.""" [[exercises]] name = "structs1" dir = "07_structs" -mode = "test" hint = """ Rust has more than one type of struct. Three actually, all variants are used to package related data together. @@ -461,7 +447,6 @@ https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" [[exercises]] name = "structs2" dir = "07_structs" -mode = "test" hint = """ Creating instances of structs is easy, all you need to do is assign some values to its fields. @@ -473,7 +458,6 @@ https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-ins [[exercises]] name = "structs3" dir = "07_structs" -mode = "test" hint = """ For `is_international`: What makes a package international? Seems related to the places it goes through right? @@ -489,13 +473,13 @@ https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" [[exercises]] name = "enums1" dir = "08_enums" -mode = "run" +test = false hint = "No hints this time ;)" [[exercises]] name = "enums2" dir = "08_enums" -mode = "run" +test = false hint = """ You can create enumerations that have different variants with different types such as no data, anonymous structs, a single string, tuples, ...etc""" @@ -503,7 +487,6 @@ such as no data, anonymous structs, a single string, tuples, ...etc""" [[exercises]] name = "enums3" dir = "08_enums" -mode = "test" hint = """ As a first step, you can define enums to compile this code without errors. @@ -517,7 +500,7 @@ to get value in the variant.""" [[exercises]] name = "strings1" dir = "09_strings" -mode = "run" +test = false hint = """ The `current_favorite_color` function is currently returning a string slice with the `'static` lifetime. We know this because the data of the string lives @@ -531,7 +514,7 @@ another way that uses the `From` trait.""" [[exercises]] name = "strings2" dir = "09_strings" -mode = "run" +test = false hint = """ Yes, it would be really easy to fix this by just changing the value bound to `word` to be a string slice instead of a `String`, wouldn't it?? There is a way @@ -546,7 +529,6 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion [[exercises]] name = "strings3" dir = "09_strings" -mode = "test" hint = """ There's tons of useful standard library functions for strings. Let's try and use some of them: https://doc.rust-lang.org/std/string/struct.String.html#method.trim @@ -557,7 +539,7 @@ the string slice into an owned string, which you can then freely extend.""" [[exercises]] name = "strings4" dir = "09_strings" -mode = "run" +test = false hint = "No hints this time ;)" # MODULES @@ -565,7 +547,7 @@ hint = "No hints this time ;)" [[exercises]] name = "modules1" dir = "10_modules" -mode = "run" +test = false hint = """ Everything is private in Rust by default-- but there's a keyword we can use to make something public! The compiler error should point to the thing that @@ -574,7 +556,7 @@ needs to be public.""" [[exercises]] name = "modules2" dir = "10_modules" -mode = "run" +test = false hint = """ The delicious_snacks module is trying to present an external interface that is different than its internal structure (the `fruits` and `veggies` modules and @@ -586,7 +568,7 @@ Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-w [[exercises]] name = "modules3" dir = "10_modules" -mode = "run" +test = false hint = """ `UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `use` statement for these two to bring them into scope. You can use nested @@ -597,7 +579,6 @@ paths or the glob operator to bring these two in using only one line.""" [[exercises]] name = "hashmaps1" dir = "11_hashmaps" -mode = "test" hint = """ Hint 1: Take a look at the return type of the function to figure out the type for the `basket`. @@ -609,7 +590,6 @@ Hint 2: Number of fruits should be at least 5. And you have to put [[exercises]] name = "hashmaps2" dir = "11_hashmaps" -mode = "test" hint = """ Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value @@ -618,7 +598,6 @@ Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only- [[exercises]] name = "hashmaps3" dir = "11_hashmaps" -mode = "test" hint = """ Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert entries corresponding to each team in the scores table. @@ -636,7 +615,6 @@ Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-v [[exercises]] name = "quiz2" dir = "quizzes" -mode = "test" hint = "No hints this time ;)" # OPTIONS @@ -644,7 +622,6 @@ hint = "No hints this time ;)" [[exercises]] name = "options1" dir = "12_options" -mode = "test" hint = """ Options can have a `Some` value, with an inner value, or a `None` value, without an inner value. @@ -656,7 +633,6 @@ it doesn't panic in your face later?""" [[exercises]] name = "options2" dir = "12_options" -mode = "test" hint = """ Check out: @@ -673,7 +649,7 @@ Also see `Option::flatten` [[exercises]] name = "options3" dir = "12_options" -mode = "run" +test = false hint = """ The compiler says a partial move happened in the `match` statement. How can this be avoided? The compiler shows the correction needed. @@ -686,7 +662,6 @@ https://doc.rust-lang.org/std/keyword.ref.html""" [[exercises]] name = "errors1" dir = "13_error_handling" -mode = "test" hint = """ `Ok` and `Err` are the two variants of `Result`, so what the tests are saying is that `generate_nametag_text` should return a `Result` instead of an `Option`. @@ -702,7 +677,6 @@ To make this change, you'll need to: [[exercises]] name = "errors2" dir = "13_error_handling" -mode = "test" hint = """ One way to handle this is using a `match` statement on `item_quantity.parse::()` where the cases are `Ok(something)` and @@ -718,7 +692,7 @@ and give it a try!""" [[exercises]] name = "errors3" dir = "13_error_handling" -mode = "run" +test = false hint = """ If other functions can return a `Result`, why shouldn't `main`? It's a fairly common convention to return something like `Result<(), ErrorType>` from your @@ -730,7 +704,6 @@ positive results.""" [[exercises]] name = "errors4" dir = "13_error_handling" -mode = "test" hint = """ `PositiveNonzeroInteger::new` is always creating a new instance and returning an `Ok` result. @@ -742,7 +715,7 @@ everything is... okay :)""" [[exercises]] name = "errors5" dir = "13_error_handling" -mode = "run" +test = false hint = """ There are two different possible `Result` types produced within `main()`, which are propagated using `?` operators. How do we declare a return type from @@ -766,7 +739,6 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen [[exercises]] name = "errors6" dir = "13_error_handling" -mode = "test" hint = """ This exercise uses a completed version of `PositiveNonzeroInteger` from errors4. @@ -788,7 +760,7 @@ https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" [[exercises]] name = "generics1" dir = "14_generics" -mode = "run" +test = false hint = """ Vectors in Rust make use of generics to create dynamically sized arrays of any type. @@ -798,7 +770,6 @@ You need to tell the compiler what type we are pushing onto this vector.""" [[exercises]] name = "generics2" dir = "14_generics" -mode = "test" hint = """ Currently we are wrapping only values of type `u32`. @@ -812,7 +783,6 @@ If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html [[exercises]] name = "traits1" dir = "15_traits" -mode = "test" hint = """ A discussion about Traits in Rust can be found at: https://doc.rust-lang.org/book/ch10-02-traits.html @@ -821,7 +791,6 @@ https://doc.rust-lang.org/book/ch10-02-traits.html [[exercises]] name = "traits2" dir = "15_traits" -mode = "test" hint = """ Notice how the trait takes ownership of `self`, and returns `Self`. @@ -834,7 +803,6 @@ the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" [[exercises]] name = "traits3" dir = "15_traits" -mode = "test" hint = """ Traits can have a default implementation for functions. Structs that implement the trait can then use the default version of these functions if they choose not @@ -846,7 +814,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#def [[exercises]] name = "traits4" dir = "15_traits" -mode = "test" hint = """ Instead of using concrete types as parameters you can use traits. Try replacing the '??' with 'impl ' @@ -857,7 +824,7 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#tra [[exercises]] name = "traits5" dir = "15_traits" -mode = "run" +test = false hint = """ To ensure a parameter implements multiple traits use the '+ syntax'. Try replacing the '??' with 'impl <> + <>'. @@ -870,7 +837,6 @@ See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#spe [[exercises]] name = "quiz3" dir = "quizzes" -mode = "test" hint = """ To find the best solution to this challenge you're going to need to think back to your knowledge of traits, specifically 'Trait Bound Syntax' @@ -882,7 +848,7 @@ You may also need this: `use std::fmt::Display;`.""" [[exercises]] name = "lifetimes1" dir = "16_lifetimes" -mode = "run" +test = false hint = """ Let the compiler guide you. Also take a look at the book if you need help: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" @@ -890,7 +856,7 @@ https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" [[exercises]] name = "lifetimes2" dir = "16_lifetimes" -mode = "run" +test = false hint = """ Remember that the generic lifetime `'a` will get the concrete lifetime that is equal to the smaller of the lifetimes of `x` and `y`. @@ -904,7 +870,7 @@ inner block: [[exercises]] name = "lifetimes3" dir = "16_lifetimes" -mode = "run" +test = false hint = """ If you use a lifetime annotation in a struct's fields, where else does it need to be added?""" @@ -914,7 +880,6 @@ to be added?""" [[exercises]] name = "tests1" dir = "17_tests" -mode = "test" hint = """ You don't even need to write any code to test -- you can just test values and run that, even though you wouldn't do that in real life. :) @@ -929,7 +894,6 @@ ones pass, and which ones fail :)""" [[exercises]] name = "tests2" dir = "17_tests" -mode = "test" hint = """ Like the previous exercise, you don't need to write any code to get this test to compile and run. @@ -942,7 +906,6 @@ argument comes first and which comes second!""" [[exercises]] name = "tests3" dir = "17_tests" -mode = "test" hint = """ You can call a function right where you're passing arguments to `assert!`. So you could do something like `assert!(having_fun())`. @@ -953,7 +916,6 @@ what you're doing using `!`, like `assert!(!having_fun())`.""" [[exercises]] name = "tests4" dir = "17_tests" -mode = "test" hint = """ We expect method `Rectangle::new()` to panic for negative values. @@ -967,7 +929,6 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa [[exercises]] name = "iterators1" dir = "18_iterators" -mode = "test" hint = """ Step 1: @@ -990,7 +951,6 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. [[exercises]] name = "iterators2" dir = "18_iterators" -mode = "test" hint = """ Step 1: @@ -1016,7 +976,6 @@ powerful and very general. Rust just needs to know the desired type.""" [[exercises]] name = "iterators3" dir = "18_iterators" -mode = "test" hint = """ The `divide` function needs to return the correct error when even division is not possible. @@ -1035,7 +994,6 @@ powerful! It can make the solution to this exercise infinitely easier.""" [[exercises]] name = "iterators4" dir = "18_iterators" -mode = "test" hint = """ In an imperative language, you might write a `for` loop that updates a mutable variable. Or, you might write code utilizing recursion and a match clause. In @@ -1047,7 +1005,6 @@ Hint 2: Check out the `fold` and `rfold` methods!""" [[exercises]] name = "iterators5" dir = "18_iterators" -mode = "test" hint = """ The documentation for the `std::iter::Iterator` trait contains numerous methods that would be helpful here. @@ -1066,7 +1023,6 @@ a different method that could make your code more compact than using `fold`.""" [[exercises]] name = "box1" dir = "19_smart_pointers" -mode = "test" hint = """ Step 1: @@ -1090,7 +1046,6 @@ definition and try other types! [[exercises]] name = "rc1" dir = "19_smart_pointers" -mode = "test" hint = """ This is a straightforward exercise to use the `Rc` type. Each `Planet` has ownership of the `Sun`, and uses `Rc::clone()` to increment the reference count @@ -1109,7 +1064,7 @@ See more at: https://doc.rust-lang.org/book/ch15-04-rc.html [[exercises]] name = "arc1" dir = "19_smart_pointers" -mode = "run" +test = false hint = """ Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order to avoid creating a copy of `numbers`, you'll need to create `child_numbers` @@ -1127,7 +1082,6 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html [[exercises]] name = "cow1" dir = "19_smart_pointers" -mode = "test" hint = """ If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is called. @@ -1141,7 +1095,7 @@ on the `Cow` type. [[exercises]] name = "threads1" dir = "20_threads" -mode = "run" +test = false hint = """ `JoinHandle` is a struct that is returned from a spawned thread: https://doc.rust-lang.org/std/thread/fn.spawn.html @@ -1159,7 +1113,7 @@ https://doc.rust-lang.org/std/thread/struct.JoinHandle.html [[exercises]] name = "threads2" dir = "20_threads" -mode = "run" +test = false hint = """ `Arc` is an Atomic Reference Counted pointer that allows safe, shared access to **immutable** data. But we want to *change* the number of `jobs_completed` @@ -1181,7 +1135,6 @@ https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-betwee [[exercises]] name = "threads3" dir = "20_threads" -mode = "test" hint = """ An alternate way to handle concurrency between threads is to use an `mpsc` (multiple producer, single consumer) channel to communicate. @@ -1200,7 +1153,7 @@ See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. [[exercises]] name = "macros1" dir = "21_macros" -mode = "run" +test = false hint = """ When you call a macro, you need to add something special compared to a regular function call. If you're stuck, take a look at what's inside @@ -1209,7 +1162,7 @@ regular function call. If you're stuck, take a look at what's inside [[exercises]] name = "macros2" dir = "21_macros" -mode = "run" +test = false hint = """ Macros don't quite play by the same rules as the rest of Rust, in terms of what's available where. @@ -1220,7 +1173,7 @@ Unlike other things in Rust, the order of "where you define a macro" versus [[exercises]] name = "macros3" dir = "21_macros" -mode = "run" +test = false hint = """ In order to use a macro outside of its module, you need to do something special to the module to lift the macro out into its parent. @@ -1231,7 +1184,7 @@ exported macros, if you've seen any of those around.""" [[exercises]] name = "macros4" dir = "21_macros" -mode = "run" +test = false hint = """ You only need to add a single character to make this compile. @@ -1248,7 +1201,8 @@ https://veykril.github.io/tlborm/""" [[exercises]] name = "clippy1" dir = "22_clippy" -mode = "clippy" +test = false +strict_clippy = true hint = """ Rust stores the highest precision version of any long or infinite precision mathematical constants in the Rust standard library: @@ -1264,14 +1218,16 @@ appropriate replacement constant from `std::f32::consts`...""" [[exercises]] name = "clippy2" dir = "22_clippy" -mode = "clippy" +test = false +strict_clippy = true hint = """ `for` loops over `Option` values are more clearly expressed as an `if let`""" [[exercises]] name = "clippy3" dir = "22_clippy" -mode = "clippy" +test = false +strict_clippy = true hint = "No hints this time!" # TYPE CONVERSIONS @@ -1279,7 +1235,6 @@ hint = "No hints this time!" [[exercises]] name = "using_as" dir = "23_conversions" -mode = "test" hint = """ Use the `as` operator to cast one of the operands in the last line of the `average` function into the expected return type.""" @@ -1287,14 +1242,12 @@ Use the `as` operator to cast one of the operands in the last line of the [[exercises]] name = "from_into" dir = "23_conversions" -mode = "test" hint = """ Follow the steps provided right before the `From` implementation""" [[exercises]] name = "from_str" dir = "23_conversions" -mode = "test" hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, or an `Err` with an error if the string is not valid. @@ -1315,7 +1268,6 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen [[exercises]] name = "try_from_into" dir = "23_conversions" -mode = "test" hint = """ Follow the steps provided right before the `TryFrom` implementation. You can also use the example at @@ -1338,6 +1290,5 @@ Challenge: Can you make the `TryFrom` implementations generic over many integer [[exercises]] name = "as_ref_mut" dir = "23_conversions" -mode = "test" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/src/app_state.rs b/src/app_state.rs index 11ac8ee4..476b5a9c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -112,7 +112,8 @@ impl AppState { dir, name, path, - mode: exercise_info.mode, + test: exercise_info.test, + strict_clippy: exercise_info.strict_clippy, hint, done: false, } @@ -329,8 +330,6 @@ impl AppState { } writeln!(writer, "{}", "ok".green())?; - - output.clear(); } writer.execute(Clear(ClearType::All))?; diff --git a/src/dev/new.rs b/src/dev/new.rs index 82aba42b..8f870107 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -99,10 +99,15 @@ name = "???" # Otherwise, the path is `exercises/NAME.rs` # dir = "???" -# The mode to run the exercise in. -# The mode "test" (preferred) runs the exercise's tests. -# The mode "run" only checks if the exercise compiles and runs it. -mode = "test" +# Rustlings expects the exercise to contain tests and run them. +# You can optionally disable testing by setting `test` to `false` (the default is `true`). +# In that case, the exercise will be considered done when it just successfully compiles. +# test = true + +# Rustlings will always run Clippy on exercises. +# You can optionally set `strict_clippy` to `true` (the default is `false`) to only consider +# the exercise as done when there are no warnings left. +# strict_clippy = false # A multi-line hint to be shown to users on request. hint = """???""" diff --git a/src/exercise.rs b/src/exercise.rs index 17cc8d77..366dc2b6 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -3,24 +3,38 @@ use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, io::{Read, Write}, - process::Command, + process::{Command, Stdio}, }; -use crate::{in_official_repo, info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; +use crate::{in_official_repo, terminal_link::TerminalFileLink, DEBUG_PROFILE}; // TODO pub const OUTPUT_CAPACITY: usize = 1 << 12; -fn run_command(mut cmd: Command, cmd_description: &str, output: &mut Vec) -> Result { +fn run_command( + mut cmd: Command, + cmd_description: &str, + output: &mut Vec, + stderr: bool, +) -> Result { let (mut reader, writer) = os_pipe::pipe().with_context(|| { format!("Failed to create a pipe to run the command `{cmd_description}``") })?; + let (stdout, stderr) = if stderr { + ( + Stdio::from(writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{cmd_description}`") + })?), + Stdio::from(writer), + ) + } else { + (Stdio::from(writer), Stdio::null()) + }; + let mut handle = cmd - .stdout(writer.try_clone().with_context(|| { - format!("Failed to clone the pipe writer for the command `{cmd_description}`") - })?) - .stderr(writer) + .stdout(stdout) + .stderr(stderr) .spawn() .with_context(|| format!("Failed to run the command `{cmd_description}`"))?; @@ -45,8 +59,8 @@ pub struct Exercise { pub name: &'static str, // Exercise's path pub path: &'static str, - // The mode of the exercise - pub mode: Mode, + pub test: bool, + pub strict_clippy: bool, // The hint text associated with the exercise pub hint: String, pub done: bool, @@ -54,10 +68,22 @@ pub struct Exercise { impl Exercise { fn run_bin(&self, output: &mut Vec) -> Result { - writeln!(output, "{}", "Output".bold().magenta().underlined())?; + writeln!(output, "{}", "Output".underlined())?; let bin_path = format!("target/debug/{}", self.name); - run_command(Command::new(&bin_path), &bin_path, output) + let success = run_command(Command::new(&bin_path), &bin_path, output, true)?; + + if !success { + writeln!( + output, + "{}", + "The exercise didn't run successfully (nonzero exit code)" + .bold() + .red() + )?; + } + + Ok(success) } fn cargo_cmd( @@ -67,6 +93,7 @@ impl Exercise { cmd_description: &str, output: &mut Vec, dev: bool, + stderr: bool, ) -> Result { let mut cmd = Command::new("cargo"); cmd.arg(command); @@ -86,25 +113,7 @@ impl Exercise { .arg(self.name) .args(args); - run_command(cmd, cmd_description, output) - } - - fn cargo_cmd_with_bin_output( - &self, - command: &str, - args: &[&str], - cmd_description: &str, - output: &mut Vec, - dev: bool, - ) -> Result { - // Discard the output of `cargo build` because it will be shown again by the Cargo command. - output.clear(); - - let cargo_cmd_success = self.cargo_cmd(command, args, cmd_description, output, dev)?; - - let run_success = self.run_bin(output)?; - - Ok(cargo_cmd_success && run_success) + run_command(cmd, cmd_description, output, stderr) } pub fn run(&self, output: &mut Vec) -> Result { @@ -113,35 +122,49 @@ impl Exercise { // Developing the official Rustlings. let dev = DEBUG_PROFILE && in_official_repo(); - let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev)?; + let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev, true)?; if !build_success { return Ok(false); } - match self.mode { - Mode::Run => self.run_bin(output), - Mode::Test => self.cargo_cmd_with_bin_output( - "test", - &[ - "--", - "--color", - "always", - "--nocapture", - "--format", - "pretty", - ], - "cargo test …", - output, - dev, - ), - Mode::Clippy => self.cargo_cmd_with_bin_output( - "clippy", - &["--", "-D", "warnings"], - "cargo clippy …", - output, - dev, - ), + // Discard the output of `cargo build` because it will be shown again by the Cargo command. + output.clear(); + + let clippy_args: &[&str] = if self.strict_clippy { + &["--", "-D", "warnings"] + } else { + &[] + }; + let clippy_success = + self.cargo_cmd("clippy", clippy_args, "cargo clippy …", output, dev, true)?; + if !clippy_success { + return Ok(false); } + + if !self.test { + return self.run_bin(output); + } + + let test_success = self.cargo_cmd( + "test", + &[ + "--", + "--color", + "always", + "--nocapture", + "--format", + "pretty", + ], + "cargo test …", + output, + dev, + // Hide warnings because they are shown by Clippy. + false, + )?; + + let run_success = self.run_bin(output)?; + + Ok(test_success && run_success) } pub fn terminal_link(&self) -> StyledContent> { diff --git a/src/info_file.rs b/src/info_file.rs index 6938cd03..dbe4f089 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,18 +2,6 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; use std::{fs, io::ErrorKind}; -// The mode of the exercise. -#[derive(Deserialize, Copy, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Mode { - // The exercise should be compiled as a binary - Run, - // The exercise should be compiled as a test harness - Test, - // The exercise should be linted with clippy - Clippy, -} - // Deserialized from the `info.toml` file. #[derive(Deserialize)] pub struct ExerciseInfo { @@ -21,11 +9,17 @@ pub struct ExerciseInfo { pub name: String, // The exercise's directory inside the `exercises` directory pub dir: Option, - // The mode of the exercise - pub mode: Mode, + #[serde(default = "default_true")] + pub test: bool, + #[serde(default)] + pub strict_clippy: bool, // The hint text associated with the exercise pub hint: String, } +#[inline] +const fn default_true() -> bool { + true +} impl ExerciseInfo { pub fn path(&self) -> String { diff --git a/tests/fixture/failure/info.toml b/tests/fixture/failure/info.toml index ef99a07a..554607a8 100644 --- a/tests/fixture/failure/info.toml +++ b/tests/fixture/failure/info.toml @@ -2,10 +2,9 @@ format_version = 1 [[exercises]] name = "compFailure" -mode = "run" +test = false hint = "" [[exercises]] name = "testFailure" -mode = "test" hint = "Hello!" diff --git a/tests/fixture/state/info.toml b/tests/fixture/state/info.toml index eec24ea1..ff0b932e 100644 --- a/tests/fixture/state/info.toml +++ b/tests/fixture/state/info.toml @@ -2,15 +2,14 @@ format_version = 1 [[exercises]] name = "pending_exercise" -mode = "run" +test = false hint = """""" [[exercises]] name = "pending_test_exercise" -mode = "test" hint = """""" [[exercises]] name = "finished_exercise" -mode = "run" +test = false hint = """""" diff --git a/tests/fixture/success/info.toml b/tests/fixture/success/info.toml index 88650ecb..d66d7d47 100644 --- a/tests/fixture/success/info.toml +++ b/tests/fixture/success/info.toml @@ -2,10 +2,9 @@ format_version = 1 [[exercises]] name = "compSuccess" -mode = "run" +test = false hint = """""" [[exercises]] name = "testSuccess" -mode = "test" hint = """""" From d26f47ddddaabcde36d5cb02ec220ccd1c141f10 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 03:27:41 +0200 Subject: [PATCH 214/433] Fix tests --- tests/fixture/failure/exercises/compNoExercise.rs | 3 +-- tests/fixture/failure/exercises/testFailure.rs | 2 ++ tests/fixture/failure/exercises/testNotPassed.rs | 2 ++ tests/fixture/state/exercises/finished_exercise.rs | 6 +----- tests/fixture/state/exercises/pending_exercise.rs | 6 +----- tests/fixture/state/exercises/pending_test_exercise.rs | 2 ++ tests/fixture/success/exercises/testSuccess.rs | 2 ++ 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/fixture/failure/exercises/compNoExercise.rs b/tests/fixture/failure/exercises/compNoExercise.rs index f79c691f..f328e4d9 100644 --- a/tests/fixture/failure/exercises/compNoExercise.rs +++ b/tests/fixture/failure/exercises/compNoExercise.rs @@ -1,2 +1 @@ -fn main() { -} +fn main() {} diff --git a/tests/fixture/failure/exercises/testFailure.rs b/tests/fixture/failure/exercises/testFailure.rs index b33a5d28..fcbcf90a 100644 --- a/tests/fixture/failure/exercises/testFailure.rs +++ b/tests/fixture/failure/exercises/testFailure.rs @@ -1,3 +1,5 @@ +fn main() {} + #[test] fn passing() { asset!(true); diff --git a/tests/fixture/failure/exercises/testNotPassed.rs b/tests/fixture/failure/exercises/testNotPassed.rs index a9fe88dd..de0d61c0 100644 --- a/tests/fixture/failure/exercises/testNotPassed.rs +++ b/tests/fixture/failure/exercises/testNotPassed.rs @@ -1,3 +1,5 @@ +fn main() {} + #[test] fn not_passing() { assert!(false); diff --git a/tests/fixture/state/exercises/finished_exercise.rs b/tests/fixture/state/exercises/finished_exercise.rs index 016b827c..f328e4d9 100644 --- a/tests/fixture/state/exercises/finished_exercise.rs +++ b/tests/fixture/state/exercises/finished_exercise.rs @@ -1,5 +1 @@ -// fake_exercise - -fn main() { - -} +fn main() {} diff --git a/tests/fixture/state/exercises/pending_exercise.rs b/tests/fixture/state/exercises/pending_exercise.rs index 016b827c..f328e4d9 100644 --- a/tests/fixture/state/exercises/pending_exercise.rs +++ b/tests/fixture/state/exercises/pending_exercise.rs @@ -1,5 +1 @@ -// fake_exercise - -fn main() { - -} +fn main() {} diff --git a/tests/fixture/state/exercises/pending_test_exercise.rs b/tests/fixture/state/exercises/pending_test_exercise.rs index 2002ef17..718e1dbb 100644 --- a/tests/fixture/state/exercises/pending_test_exercise.rs +++ b/tests/fixture/state/exercises/pending_test_exercise.rs @@ -1,2 +1,4 @@ +fn main() {} + #[test] fn it_works() {} diff --git a/tests/fixture/success/exercises/testSuccess.rs b/tests/fixture/success/exercises/testSuccess.rs index 7139b50b..4296cf61 100644 --- a/tests/fixture/success/exercises/testSuccess.rs +++ b/tests/fixture/success/exercises/testSuccess.rs @@ -1,3 +1,5 @@ +fn main() {} + #[test] fn passing() { println!("THIS TEST TOO SHALL PASS"); From 428998a4cf74246b0a1da4b8013b504d86cdabeb Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 03:28:44 +0200 Subject: [PATCH 215/433] Quicker response to file changes --- src/watch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watch.rs b/src/watch.rs index 5ebe526f..1e22f592 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -51,7 +51,7 @@ pub fn watch( // Otherwise, the file watcher exits. let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { let mut debouncer = new_debouncer( - Duration::from_secs(1), + Duration::from_millis(500), DebounceEventHandler { tx: tx.clone(), exercise_paths, From 1f1a62d83ef9398a1a31c904a2ef6d81f5455e59 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 14:43:02 +0200 Subject: [PATCH 216/433] Raise the output capacity --- src/exercise.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 366dc2b6..21ae5828 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -8,8 +8,7 @@ use std::{ use crate::{in_official_repo, terminal_link::TerminalFileLink, DEBUG_PROFILE}; -// TODO -pub const OUTPUT_CAPACITY: usize = 1 << 12; +pub const OUTPUT_CAPACITY: usize = 1 << 14; fn run_command( mut cmd: Command, From 14fe248b4bfc3c577be7deacc346a959c7c0cc47 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 14:44:12 +0200 Subject: [PATCH 217/433] Optimize the notify event handler --- src/main.rs | 8 ++++---- src/watch.rs | 6 +++--- src/watch/notify_event.rs | 21 +++++++++++++++------ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index a9285044..7a142fdb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -144,23 +144,23 @@ fn main() -> Result<()> { match args.command { None => { - let notify_exercise_paths: Option<&'static [&'static str]> = if args.manual_run { + let notify_exercise_names = if args.manual_run { None } else { // For the the notify event handler thread. // Leaking is not a problem because the slice lives until the end of the program. Some( - app_state + &*app_state .exercises() .iter() - .map(|exercise| exercise.path) + .map(|exercise| exercise.name.as_bytes()) .collect::>() .leak(), ) }; loop { - match watch::watch(&mut app_state, notify_exercise_paths)? { + match watch::watch(&mut app_state, notify_exercise_names)? { WatchExit::Shutdown => break, // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the diff --git a/src/watch.rs b/src/watch.rs index 1e22f592..2f4409ac 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -42,19 +42,19 @@ pub enum WatchExit { pub fn watch( app_state: &mut AppState, - notify_exercise_paths: Option<&'static [&'static str]>, + notify_exercise_names: Option<&'static [&'static [u8]]>, ) -> Result { let (tx, rx) = channel(); let mut manual_run = false; // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. - let _debouncer_guard = if let Some(exercise_paths) = notify_exercise_paths { + let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( Duration::from_millis(500), DebounceEventHandler { tx: tx.clone(), - exercise_paths, + exercise_names, }, ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index fb9a8c05..f66a8342 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -5,7 +5,7 @@ use super::WatchEvent; pub struct DebounceEventHandler { pub tx: Sender, - pub exercise_paths: &'static [&'static str], + pub exercise_names: &'static [&'static [u8]], } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { @@ -15,15 +15,24 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { let Some(exercise_ind) = event .iter() .filter_map(|event| { - if event.kind != DebouncedEventKind::Any - || !event.path.extension().is_some_and(|ext| ext == "rs") - { + if event.kind != DebouncedEventKind::Any { return None; } - self.exercise_paths + let file_name = event.path.file_name()?.to_str()?.as_bytes(); + + if file_name.len() < 4 { + return None; + } + let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); + + if ext != b".rs" { + return None; + } + + self.exercise_names .iter() - .position(|path| event.path.ends_with(path)) + .position(|exercise_name| *exercise_name == file_name_without_ext) }) .min() else { From c1d28b502eb88aa06acf196ff055f285a8482132 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 14:51:14 +0200 Subject: [PATCH 218/433] Format test file :P --- tests/fixture/success/exercises/compSuccess.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/fixture/success/exercises/compSuccess.rs b/tests/fixture/success/exercises/compSuccess.rs index f79c691f..f328e4d9 100644 --- a/tests/fixture/success/exercises/compSuccess.rs +++ b/tests/fixture/success/exercises/compSuccess.rs @@ -1,2 +1 @@ -fn main() { -} +fn main() {} From c7c8d9968040d7df438fa33b32d9495415f751d2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:22:11 +0200 Subject: [PATCH 219/433] Moar responsive :P --- src/watch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watch.rs b/src/watch.rs index 2f4409ac..5c3f1709 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -51,7 +51,7 @@ pub fn watch( // Otherwise, the file watcher exits. let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( - Duration::from_millis(500), + Duration::from_millis(200), DebounceEventHandler { tx: tx.clone(), exercise_names, From 29abaee4ec22627475aeabaf8425f759d2463530 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:22:14 +0200 Subject: [PATCH 220/433] Update dep --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 823fec36..23c4887a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -965,9 +965,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134306a13c5647ad6453e8deaec55d3a44d6021970129e6188735e74bf546697" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ "windows-sys 0.52.0", ] From b3b4b7d59c5ecbf9658f7cd2eae85ee1c9e41e73 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:23:24 +0200 Subject: [PATCH 221/433] Update initialized .gitignore --- src/init.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/init.rs b/src/init.rs index f1a95099..eeb1c292 100644 --- a/src/init.rs +++ b/src/init.rs @@ -55,9 +55,10 @@ pub fn init() -> Result<()> { Ok(()) } -pub const GITIGNORE: &[u8] = b"Cargo.lock -.rustlings-state.txt +pub const GITIGNORE: &[u8] = b".rustlings-state.txt +Cargo.lock target +.vscode "; pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; From fcefa3d6144028a77ed381ddaabcf004a02c804c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:33:24 +0200 Subject: [PATCH 222/433] Name the exercises' package `exercises` --- dev/Cargo.toml | 3 ++- src/dev/new.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index eddf0164..8da41a56 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -99,6 +99,7 @@ bin = [ ] [package] -name = "rustlings-dev" +name = "exercises" edition = "2021" +# Don't publish the exercises on crates.io! publish = false diff --git a/src/dev/new.rs b/src/dev/new.rs index 8f870107..c5a0bd27 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -118,8 +118,9 @@ const CARGO_TOML: &[u8] = bin = [] [package] -name = "rustlings" +name = "exercises" edition = "2021" +# Don't publish the exercises on crates.io! publish = false [dependencies] From 212c82c6f6a0356ed6b292ddc48a8444e8e9dbf2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:34:58 +0200 Subject: [PATCH 223/433] Don't ignore .vscode/extensions.json when developing third-party exercises --- src/dev/new.rs | 9 ++++++++- src/init.rs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dev/new.rs b/src/dev/new.rs index c5a0bd27..44487abf 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -45,7 +45,7 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { bail!("`git init` didn't run successfully. See the error message above"); } - write_rel_file(".gitignore", &dir_name, crate::init::GITIGNORE)?; + write_rel_file(".gitignore", &dir_name, GITIGNORE)?; create_rel_dir("exercises", &dir_name)?; create_rel_dir("solutions", &dir_name)?; @@ -72,6 +72,13 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { Ok(()) } +pub const GITIGNORE: &[u8] = b".rustlings-state.txt +Cargo.lock +target +.vscode +!.vscode/extensions.json +"; + const INFO_FILE_BEFORE_FORMAT_VERSION: &str = "# The format version is an indicator of the compatibility of third-party exercises with the # Rustlings program. diff --git a/src/init.rs b/src/init.rs index eeb1c292..7ce1272d 100644 --- a/src/init.rs +++ b/src/init.rs @@ -55,7 +55,7 @@ pub fn init() -> Result<()> { Ok(()) } -pub const GITIGNORE: &[u8] = b".rustlings-state.txt +const GITIGNORE: &[u8] = b".rustlings-state.txt Cargo.lock target .vscode From 6d1d42d2dd4a9323b0cd1f964d1651f27a9b328a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:41:52 +0200 Subject: [PATCH 224/433] Try to run `git init` --- src/init.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/init.rs b/src/init.rs index 7ce1272d..addc5716 100644 --- a/src/init.rs +++ b/src/init.rs @@ -4,6 +4,7 @@ use std::{ fs::{self, create_dir}, io::ErrorKind, path::Path, + process::{Command, Stdio}, }; use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile}; @@ -50,6 +51,13 @@ pub fn init() -> Result<()> { fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) .context("Failed to create the file `rustlings/.vscode/extensions.json`")?; + // Ignore any Git error because Git initialization is not required. + let _ = Command::new("git") + .arg("init") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .status(); + println!("{POST_INIT_MSG}"); Ok(()) @@ -76,7 +84,8 @@ You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; -const POST_INIT_MSG: &str = "Done initialization! +const POST_INIT_MSG: &str = " +Done initialization! Run `cd rustlings` to go into the generated directory. Then run `rustlings` to get started."; From 8bf8b19a5dd4278a636a56440736c8f3df52b7a5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:51:12 +0200 Subject: [PATCH 225/433] Improve output after initialization --- src/init.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/init.rs b/src/init.rs index addc5716..ce239ea9 100644 --- a/src/init.rs +++ b/src/init.rs @@ -1,4 +1,5 @@ use anyhow::{bail, Context, Result}; +use crossterm::style::Stylize; use std::{ env::set_current_dir, fs::{self, create_dir}, @@ -58,7 +59,11 @@ pub fn init() -> Result<()> { .stderr(Stdio::null()) .status(); - println!("{POST_INIT_MSG}"); + println!( + "\n{}\n\n{}", + "Initialization done ✓".green(), + POST_INIT_MSG.bold(), + ); Ok(()) } @@ -84,8 +89,5 @@ You probably already initialized Rustlings. Run `cd rustlings` Then run `rustlings` again"; -const POST_INIT_MSG: &str = " -Done initialization! - -Run `cd rustlings` to go into the generated directory. +const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. Then run `rustlings` to get started."; From c51f1b3f31478f8c82acbd83e9ae873e29159c5f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 15:58:46 +0200 Subject: [PATCH 226/433] Thanks Clippy :D --- src/dev.rs | 6 +++--- src/dev/check.rs | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index 38338cd0..737de0de 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -28,15 +28,15 @@ pub enum DevCommands { impl DevCommands { pub fn run(self) -> Result<()> { match self { - DevCommands::New { path, no_git } => { + Self::New { path, no_git } => { if DEBUG_PROFILE { bail!("Disabled in the debug build"); } new::new(&path, no_git).context(INIT_ERR) } - DevCommands::Check => check::check(), - DevCommands::Update => update::update(), + Self::Check => check::check(), + Self::Update => update::update(), } } } diff --git a/src/dev/check.rs b/src/dev/check.rs index 9859c3e3..564aa0a2 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -151,11 +151,9 @@ fn check_cargo_toml( if old_bins != new_bins { if DEBUG_PROFILE { bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); - } else { - bail!( - "The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it", - ); } + + bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); } Ok(()) From 3ce32352945a81972b5b421d522ca5e6fbd28d2c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 16:08:07 +0200 Subject: [PATCH 227/433] Show warnings and errors in the tests --- src/exercise.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 21ae5828..50f360e9 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -130,9 +130,9 @@ impl Exercise { output.clear(); let clippy_args: &[&str] = if self.strict_clippy { - &["--", "-D", "warnings"] + &["--profile", "test", "--", "-D", "warnings"] } else { - &[] + &["--profile", "test"] }; let clippy_success = self.cargo_cmd("clippy", clippy_args, "cargo clippy …", output, dev, true)?; From 177e2870c5272dacee662d3e9522bc57c5a134c0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 16:30:01 +0200 Subject: [PATCH 228/433] Edit comment --- solutions/00_intro/intro1.rs | 2 +- solutions/00_intro/intro2.rs | 2 +- solutions/01_variables/variables1.rs | 2 +- solutions/01_variables/variables2.rs | 2 +- solutions/01_variables/variables3.rs | 2 +- solutions/01_variables/variables4.rs | 2 +- solutions/01_variables/variables5.rs | 2 +- solutions/01_variables/variables6.rs | 2 +- solutions/02_functions/functions1.rs | 2 +- solutions/02_functions/functions2.rs | 2 +- solutions/02_functions/functions3.rs | 2 +- solutions/02_functions/functions4.rs | 2 +- solutions/02_functions/functions5.rs | 2 +- solutions/03_if/if1.rs | 2 +- solutions/03_if/if2.rs | 2 +- solutions/03_if/if3.rs | 2 +- solutions/04_primitive_types/primitive_types1.rs | 2 +- solutions/04_primitive_types/primitive_types2.rs | 2 +- solutions/04_primitive_types/primitive_types3.rs | 2 +- solutions/04_primitive_types/primitive_types4.rs | 2 +- solutions/04_primitive_types/primitive_types5.rs | 2 +- solutions/04_primitive_types/primitive_types6.rs | 2 +- solutions/05_vecs/vecs1.rs | 2 +- solutions/05_vecs/vecs2.rs | 2 +- solutions/06_move_semantics/move_semantics1.rs | 2 +- solutions/06_move_semantics/move_semantics2.rs | 2 +- solutions/06_move_semantics/move_semantics3.rs | 2 +- solutions/06_move_semantics/move_semantics4.rs | 2 +- solutions/06_move_semantics/move_semantics5.rs | 2 +- solutions/06_move_semantics/move_semantics6.rs | 2 +- solutions/07_structs/structs1.rs | 2 +- solutions/07_structs/structs2.rs | 2 +- solutions/07_structs/structs3.rs | 2 +- solutions/08_enums/enums1.rs | 2 +- solutions/08_enums/enums2.rs | 2 +- solutions/08_enums/enums3.rs | 2 +- solutions/09_strings/strings1.rs | 2 +- solutions/09_strings/strings2.rs | 2 +- solutions/09_strings/strings3.rs | 2 +- solutions/09_strings/strings4.rs | 2 +- solutions/10_modules/modules1.rs | 2 +- solutions/10_modules/modules2.rs | 2 +- solutions/10_modules/modules3.rs | 2 +- solutions/11_hashmaps/hashmaps1.rs | 2 +- solutions/11_hashmaps/hashmaps2.rs | 2 +- solutions/11_hashmaps/hashmaps3.rs | 2 +- solutions/12_options/options1.rs | 2 +- solutions/12_options/options2.rs | 2 +- solutions/12_options/options3.rs | 2 +- solutions/13_error_handling/errors1.rs | 2 +- solutions/13_error_handling/errors2.rs | 2 +- solutions/13_error_handling/errors3.rs | 2 +- solutions/13_error_handling/errors4.rs | 2 +- solutions/13_error_handling/errors5.rs | 2 +- solutions/13_error_handling/errors6.rs | 2 +- solutions/14_generics/generics1.rs | 2 +- solutions/14_generics/generics2.rs | 2 +- solutions/15_traits/traits1.rs | 2 +- solutions/15_traits/traits2.rs | 2 +- solutions/15_traits/traits3.rs | 2 +- solutions/15_traits/traits4.rs | 2 +- solutions/15_traits/traits5.rs | 2 +- solutions/16_lifetimes/lifetimes1.rs | 2 +- solutions/16_lifetimes/lifetimes2.rs | 2 +- solutions/16_lifetimes/lifetimes3.rs | 2 +- solutions/17_tests/tests1.rs | 2 +- solutions/17_tests/tests2.rs | 2 +- solutions/17_tests/tests3.rs | 2 +- solutions/17_tests/tests4.rs | 2 +- solutions/18_iterators/iterators1.rs | 2 +- solutions/18_iterators/iterators2.rs | 2 +- solutions/18_iterators/iterators3.rs | 2 +- solutions/18_iterators/iterators4.rs | 2 +- solutions/18_iterators/iterators5.rs | 2 +- solutions/19_smart_pointers/arc1.rs | 2 +- solutions/19_smart_pointers/box1.rs | 2 +- solutions/19_smart_pointers/cow1.rs | 2 +- solutions/19_smart_pointers/rc1.rs | 2 +- solutions/20_threads/threads1.rs | 2 +- solutions/20_threads/threads2.rs | 2 +- solutions/20_threads/threads3.rs | 2 +- solutions/21_macros/macros1.rs | 2 +- solutions/21_macros/macros2.rs | 2 +- solutions/21_macros/macros3.rs | 2 +- solutions/21_macros/macros4.rs | 2 +- solutions/22_clippy/clippy1.rs | 2 +- solutions/22_clippy/clippy2.rs | 2 +- solutions/22_clippy/clippy3.rs | 2 +- solutions/23_conversions/as_ref_mut.rs | 2 +- solutions/23_conversions/from_into.rs | 2 +- solutions/23_conversions/from_str.rs | 2 +- solutions/23_conversions/try_from_into.rs | 2 +- solutions/23_conversions/using_as.rs | 2 +- solutions/quizzes/quiz1.rs | 2 +- solutions/quizzes/quiz2.rs | 2 +- solutions/quizzes/quiz3.rs | 2 +- 96 files changed, 96 insertions(+), 96 deletions(-) diff --git a/solutions/00_intro/intro1.rs b/solutions/00_intro/intro1.rs index 70b786d1..4e181989 100644 --- a/solutions/00_intro/intro1.rs +++ b/solutions/00_intro/intro1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/00_intro/intro2.rs b/solutions/00_intro/intro2.rs index 70b786d1..4e181989 100644 --- a/solutions/00_intro/intro2.rs +++ b/solutions/00_intro/intro2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/01_variables/variables1.rs b/solutions/01_variables/variables1.rs index 70b786d1..4e181989 100644 --- a/solutions/01_variables/variables1.rs +++ b/solutions/01_variables/variables1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/01_variables/variables2.rs b/solutions/01_variables/variables2.rs index 70b786d1..4e181989 100644 --- a/solutions/01_variables/variables2.rs +++ b/solutions/01_variables/variables2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/01_variables/variables3.rs b/solutions/01_variables/variables3.rs index 70b786d1..4e181989 100644 --- a/solutions/01_variables/variables3.rs +++ b/solutions/01_variables/variables3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/01_variables/variables4.rs b/solutions/01_variables/variables4.rs index 70b786d1..4e181989 100644 --- a/solutions/01_variables/variables4.rs +++ b/solutions/01_variables/variables4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/01_variables/variables5.rs b/solutions/01_variables/variables5.rs index 70b786d1..4e181989 100644 --- a/solutions/01_variables/variables5.rs +++ b/solutions/01_variables/variables5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/01_variables/variables6.rs b/solutions/01_variables/variables6.rs index 70b786d1..4e181989 100644 --- a/solutions/01_variables/variables6.rs +++ b/solutions/01_variables/variables6.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/02_functions/functions1.rs b/solutions/02_functions/functions1.rs index 70b786d1..4e181989 100644 --- a/solutions/02_functions/functions1.rs +++ b/solutions/02_functions/functions1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/02_functions/functions2.rs b/solutions/02_functions/functions2.rs index 70b786d1..4e181989 100644 --- a/solutions/02_functions/functions2.rs +++ b/solutions/02_functions/functions2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/02_functions/functions3.rs b/solutions/02_functions/functions3.rs index 70b786d1..4e181989 100644 --- a/solutions/02_functions/functions3.rs +++ b/solutions/02_functions/functions3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/02_functions/functions4.rs b/solutions/02_functions/functions4.rs index 70b786d1..4e181989 100644 --- a/solutions/02_functions/functions4.rs +++ b/solutions/02_functions/functions4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/02_functions/functions5.rs b/solutions/02_functions/functions5.rs index 70b786d1..4e181989 100644 --- a/solutions/02_functions/functions5.rs +++ b/solutions/02_functions/functions5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/03_if/if1.rs b/solutions/03_if/if1.rs index 70b786d1..4e181989 100644 --- a/solutions/03_if/if1.rs +++ b/solutions/03_if/if1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/03_if/if2.rs b/solutions/03_if/if2.rs index 70b786d1..4e181989 100644 --- a/solutions/03_if/if2.rs +++ b/solutions/03_if/if2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/03_if/if3.rs b/solutions/03_if/if3.rs index 70b786d1..4e181989 100644 --- a/solutions/03_if/if3.rs +++ b/solutions/03_if/if3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/04_primitive_types/primitive_types1.rs b/solutions/04_primitive_types/primitive_types1.rs index 70b786d1..4e181989 100644 --- a/solutions/04_primitive_types/primitive_types1.rs +++ b/solutions/04_primitive_types/primitive_types1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/04_primitive_types/primitive_types2.rs b/solutions/04_primitive_types/primitive_types2.rs index 70b786d1..4e181989 100644 --- a/solutions/04_primitive_types/primitive_types2.rs +++ b/solutions/04_primitive_types/primitive_types2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/04_primitive_types/primitive_types3.rs b/solutions/04_primitive_types/primitive_types3.rs index 70b786d1..4e181989 100644 --- a/solutions/04_primitive_types/primitive_types3.rs +++ b/solutions/04_primitive_types/primitive_types3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/04_primitive_types/primitive_types4.rs b/solutions/04_primitive_types/primitive_types4.rs index 70b786d1..4e181989 100644 --- a/solutions/04_primitive_types/primitive_types4.rs +++ b/solutions/04_primitive_types/primitive_types4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/04_primitive_types/primitive_types5.rs b/solutions/04_primitive_types/primitive_types5.rs index 70b786d1..4e181989 100644 --- a/solutions/04_primitive_types/primitive_types5.rs +++ b/solutions/04_primitive_types/primitive_types5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/04_primitive_types/primitive_types6.rs b/solutions/04_primitive_types/primitive_types6.rs index 70b786d1..4e181989 100644 --- a/solutions/04_primitive_types/primitive_types6.rs +++ b/solutions/04_primitive_types/primitive_types6.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/05_vecs/vecs1.rs b/solutions/05_vecs/vecs1.rs index 70b786d1..4e181989 100644 --- a/solutions/05_vecs/vecs1.rs +++ b/solutions/05_vecs/vecs1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs index 70b786d1..4e181989 100644 --- a/solutions/05_vecs/vecs2.rs +++ b/solutions/05_vecs/vecs2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/06_move_semantics/move_semantics1.rs b/solutions/06_move_semantics/move_semantics1.rs index 70b786d1..4e181989 100644 --- a/solutions/06_move_semantics/move_semantics1.rs +++ b/solutions/06_move_semantics/move_semantics1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/06_move_semantics/move_semantics2.rs b/solutions/06_move_semantics/move_semantics2.rs index 70b786d1..4e181989 100644 --- a/solutions/06_move_semantics/move_semantics2.rs +++ b/solutions/06_move_semantics/move_semantics2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/06_move_semantics/move_semantics3.rs b/solutions/06_move_semantics/move_semantics3.rs index 70b786d1..4e181989 100644 --- a/solutions/06_move_semantics/move_semantics3.rs +++ b/solutions/06_move_semantics/move_semantics3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/06_move_semantics/move_semantics4.rs b/solutions/06_move_semantics/move_semantics4.rs index 70b786d1..4e181989 100644 --- a/solutions/06_move_semantics/move_semantics4.rs +++ b/solutions/06_move_semantics/move_semantics4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/06_move_semantics/move_semantics5.rs b/solutions/06_move_semantics/move_semantics5.rs index 70b786d1..4e181989 100644 --- a/solutions/06_move_semantics/move_semantics5.rs +++ b/solutions/06_move_semantics/move_semantics5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/06_move_semantics/move_semantics6.rs b/solutions/06_move_semantics/move_semantics6.rs index 70b786d1..4e181989 100644 --- a/solutions/06_move_semantics/move_semantics6.rs +++ b/solutions/06_move_semantics/move_semantics6.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/07_structs/structs1.rs b/solutions/07_structs/structs1.rs index 70b786d1..4e181989 100644 --- a/solutions/07_structs/structs1.rs +++ b/solutions/07_structs/structs1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/07_structs/structs2.rs b/solutions/07_structs/structs2.rs index 70b786d1..4e181989 100644 --- a/solutions/07_structs/structs2.rs +++ b/solutions/07_structs/structs2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index 70b786d1..4e181989 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/08_enums/enums1.rs b/solutions/08_enums/enums1.rs index 70b786d1..4e181989 100644 --- a/solutions/08_enums/enums1.rs +++ b/solutions/08_enums/enums1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/08_enums/enums2.rs b/solutions/08_enums/enums2.rs index 70b786d1..4e181989 100644 --- a/solutions/08_enums/enums2.rs +++ b/solutions/08_enums/enums2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/08_enums/enums3.rs b/solutions/08_enums/enums3.rs index 70b786d1..4e181989 100644 --- a/solutions/08_enums/enums3.rs +++ b/solutions/08_enums/enums3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/09_strings/strings1.rs b/solutions/09_strings/strings1.rs index 70b786d1..4e181989 100644 --- a/solutions/09_strings/strings1.rs +++ b/solutions/09_strings/strings1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/09_strings/strings2.rs b/solutions/09_strings/strings2.rs index 70b786d1..4e181989 100644 --- a/solutions/09_strings/strings2.rs +++ b/solutions/09_strings/strings2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/09_strings/strings3.rs b/solutions/09_strings/strings3.rs index 70b786d1..4e181989 100644 --- a/solutions/09_strings/strings3.rs +++ b/solutions/09_strings/strings3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index 70b786d1..4e181989 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/10_modules/modules1.rs b/solutions/10_modules/modules1.rs index 70b786d1..4e181989 100644 --- a/solutions/10_modules/modules1.rs +++ b/solutions/10_modules/modules1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/10_modules/modules2.rs b/solutions/10_modules/modules2.rs index 70b786d1..4e181989 100644 --- a/solutions/10_modules/modules2.rs +++ b/solutions/10_modules/modules2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/10_modules/modules3.rs b/solutions/10_modules/modules3.rs index 70b786d1..4e181989 100644 --- a/solutions/10_modules/modules3.rs +++ b/solutions/10_modules/modules3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs index 70b786d1..4e181989 100644 --- a/solutions/11_hashmaps/hashmaps1.rs +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/11_hashmaps/hashmaps2.rs b/solutions/11_hashmaps/hashmaps2.rs index 70b786d1..4e181989 100644 --- a/solutions/11_hashmaps/hashmaps2.rs +++ b/solutions/11_hashmaps/hashmaps2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 70b786d1..4e181989 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs index 70b786d1..4e181989 100644 --- a/solutions/12_options/options1.rs +++ b/solutions/12_options/options1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/12_options/options2.rs b/solutions/12_options/options2.rs index 70b786d1..4e181989 100644 --- a/solutions/12_options/options2.rs +++ b/solutions/12_options/options2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/12_options/options3.rs b/solutions/12_options/options3.rs index 70b786d1..4e181989 100644 --- a/solutions/12_options/options3.rs +++ b/solutions/12_options/options3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/13_error_handling/errors1.rs b/solutions/13_error_handling/errors1.rs index 70b786d1..4e181989 100644 --- a/solutions/13_error_handling/errors1.rs +++ b/solutions/13_error_handling/errors1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/13_error_handling/errors2.rs b/solutions/13_error_handling/errors2.rs index 70b786d1..4e181989 100644 --- a/solutions/13_error_handling/errors2.rs +++ b/solutions/13_error_handling/errors2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/13_error_handling/errors3.rs b/solutions/13_error_handling/errors3.rs index 70b786d1..4e181989 100644 --- a/solutions/13_error_handling/errors3.rs +++ b/solutions/13_error_handling/errors3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs index 70b786d1..4e181989 100644 --- a/solutions/13_error_handling/errors4.rs +++ b/solutions/13_error_handling/errors4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs index 70b786d1..4e181989 100644 --- a/solutions/13_error_handling/errors5.rs +++ b/solutions/13_error_handling/errors5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs index 70b786d1..4e181989 100644 --- a/solutions/13_error_handling/errors6.rs +++ b/solutions/13_error_handling/errors6.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/14_generics/generics1.rs b/solutions/14_generics/generics1.rs index 70b786d1..4e181989 100644 --- a/solutions/14_generics/generics1.rs +++ b/solutions/14_generics/generics1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/14_generics/generics2.rs b/solutions/14_generics/generics2.rs index 70b786d1..4e181989 100644 --- a/solutions/14_generics/generics2.rs +++ b/solutions/14_generics/generics2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/15_traits/traits1.rs b/solutions/15_traits/traits1.rs index 70b786d1..4e181989 100644 --- a/solutions/15_traits/traits1.rs +++ b/solutions/15_traits/traits1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/15_traits/traits2.rs b/solutions/15_traits/traits2.rs index 70b786d1..4e181989 100644 --- a/solutions/15_traits/traits2.rs +++ b/solutions/15_traits/traits2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/15_traits/traits3.rs b/solutions/15_traits/traits3.rs index 70b786d1..4e181989 100644 --- a/solutions/15_traits/traits3.rs +++ b/solutions/15_traits/traits3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/15_traits/traits4.rs b/solutions/15_traits/traits4.rs index 70b786d1..4e181989 100644 --- a/solutions/15_traits/traits4.rs +++ b/solutions/15_traits/traits4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/15_traits/traits5.rs b/solutions/15_traits/traits5.rs index 70b786d1..4e181989 100644 --- a/solutions/15_traits/traits5.rs +++ b/solutions/15_traits/traits5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/16_lifetimes/lifetimes1.rs b/solutions/16_lifetimes/lifetimes1.rs index 70b786d1..4e181989 100644 --- a/solutions/16_lifetimes/lifetimes1.rs +++ b/solutions/16_lifetimes/lifetimes1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs index 70b786d1..4e181989 100644 --- a/solutions/16_lifetimes/lifetimes2.rs +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/16_lifetimes/lifetimes3.rs b/solutions/16_lifetimes/lifetimes3.rs index 70b786d1..4e181989 100644 --- a/solutions/16_lifetimes/lifetimes3.rs +++ b/solutions/16_lifetimes/lifetimes3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/17_tests/tests1.rs b/solutions/17_tests/tests1.rs index 70b786d1..4e181989 100644 --- a/solutions/17_tests/tests1.rs +++ b/solutions/17_tests/tests1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/17_tests/tests2.rs b/solutions/17_tests/tests2.rs index 70b786d1..4e181989 100644 --- a/solutions/17_tests/tests2.rs +++ b/solutions/17_tests/tests2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/17_tests/tests3.rs b/solutions/17_tests/tests3.rs index 70b786d1..4e181989 100644 --- a/solutions/17_tests/tests3.rs +++ b/solutions/17_tests/tests3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/17_tests/tests4.rs b/solutions/17_tests/tests4.rs index 70b786d1..4e181989 100644 --- a/solutions/17_tests/tests4.rs +++ b/solutions/17_tests/tests4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs index 70b786d1..4e181989 100644 --- a/solutions/18_iterators/iterators1.rs +++ b/solutions/18_iterators/iterators1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/18_iterators/iterators2.rs b/solutions/18_iterators/iterators2.rs index 70b786d1..4e181989 100644 --- a/solutions/18_iterators/iterators2.rs +++ b/solutions/18_iterators/iterators2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/18_iterators/iterators3.rs b/solutions/18_iterators/iterators3.rs index 70b786d1..4e181989 100644 --- a/solutions/18_iterators/iterators3.rs +++ b/solutions/18_iterators/iterators3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/18_iterators/iterators4.rs b/solutions/18_iterators/iterators4.rs index 70b786d1..4e181989 100644 --- a/solutions/18_iterators/iterators4.rs +++ b/solutions/18_iterators/iterators4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/18_iterators/iterators5.rs b/solutions/18_iterators/iterators5.rs index 70b786d1..4e181989 100644 --- a/solutions/18_iterators/iterators5.rs +++ b/solutions/18_iterators/iterators5.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/19_smart_pointers/arc1.rs b/solutions/19_smart_pointers/arc1.rs index 70b786d1..4e181989 100644 --- a/solutions/19_smart_pointers/arc1.rs +++ b/solutions/19_smart_pointers/arc1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/19_smart_pointers/box1.rs b/solutions/19_smart_pointers/box1.rs index 70b786d1..4e181989 100644 --- a/solutions/19_smart_pointers/box1.rs +++ b/solutions/19_smart_pointers/box1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/19_smart_pointers/cow1.rs b/solutions/19_smart_pointers/cow1.rs index 70b786d1..4e181989 100644 --- a/solutions/19_smart_pointers/cow1.rs +++ b/solutions/19_smart_pointers/cow1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/rc1.rs index 70b786d1..4e181989 100644 --- a/solutions/19_smart_pointers/rc1.rs +++ b/solutions/19_smart_pointers/rc1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/20_threads/threads1.rs b/solutions/20_threads/threads1.rs index 70b786d1..4e181989 100644 --- a/solutions/20_threads/threads1.rs +++ b/solutions/20_threads/threads1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/20_threads/threads2.rs b/solutions/20_threads/threads2.rs index 70b786d1..4e181989 100644 --- a/solutions/20_threads/threads2.rs +++ b/solutions/20_threads/threads2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/20_threads/threads3.rs b/solutions/20_threads/threads3.rs index 70b786d1..4e181989 100644 --- a/solutions/20_threads/threads3.rs +++ b/solutions/20_threads/threads3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/21_macros/macros1.rs b/solutions/21_macros/macros1.rs index 70b786d1..4e181989 100644 --- a/solutions/21_macros/macros1.rs +++ b/solutions/21_macros/macros1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/21_macros/macros2.rs b/solutions/21_macros/macros2.rs index 70b786d1..4e181989 100644 --- a/solutions/21_macros/macros2.rs +++ b/solutions/21_macros/macros2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs index 70b786d1..4e181989 100644 --- a/solutions/21_macros/macros3.rs +++ b/solutions/21_macros/macros3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/21_macros/macros4.rs b/solutions/21_macros/macros4.rs index 70b786d1..4e181989 100644 --- a/solutions/21_macros/macros4.rs +++ b/solutions/21_macros/macros4.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/22_clippy/clippy1.rs b/solutions/22_clippy/clippy1.rs index 70b786d1..4e181989 100644 --- a/solutions/22_clippy/clippy1.rs +++ b/solutions/22_clippy/clippy1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/22_clippy/clippy2.rs b/solutions/22_clippy/clippy2.rs index 70b786d1..4e181989 100644 --- a/solutions/22_clippy/clippy2.rs +++ b/solutions/22_clippy/clippy2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/22_clippy/clippy3.rs b/solutions/22_clippy/clippy3.rs index 70b786d1..4e181989 100644 --- a/solutions/22_clippy/clippy3.rs +++ b/solutions/22_clippy/clippy3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/as_ref_mut.rs index 70b786d1..4e181989 100644 --- a/solutions/23_conversions/as_ref_mut.rs +++ b/solutions/23_conversions/as_ref_mut.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/23_conversions/from_into.rs b/solutions/23_conversions/from_into.rs index 70b786d1..4e181989 100644 --- a/solutions/23_conversions/from_into.rs +++ b/solutions/23_conversions/from_into.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/from_str.rs index 70b786d1..4e181989 100644 --- a/solutions/23_conversions/from_str.rs +++ b/solutions/23_conversions/from_str.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/try_from_into.rs index 70b786d1..4e181989 100644 --- a/solutions/23_conversions/try_from_into.rs +++ b/solutions/23_conversions/try_from_into.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/23_conversions/using_as.rs b/solutions/23_conversions/using_as.rs index 70b786d1..4e181989 100644 --- a/solutions/23_conversions/using_as.rs +++ b/solutions/23_conversions/using_as.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/quizzes/quiz1.rs b/solutions/quizzes/quiz1.rs index 70b786d1..4e181989 100644 --- a/solutions/quizzes/quiz1.rs +++ b/solutions/quizzes/quiz1.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/quizzes/quiz2.rs b/solutions/quizzes/quiz2.rs index 70b786d1..4e181989 100644 --- a/solutions/quizzes/quiz2.rs +++ b/solutions/quizzes/quiz2.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 diff --git a/solutions/quizzes/quiz3.rs b/solutions/quizzes/quiz3.rs index 70b786d1..4e181989 100644 --- a/solutions/quizzes/quiz3.rs +++ b/solutions/quizzes/quiz3.rs @@ -1 +1 @@ -// TODO +// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 From ca41f9e2df5512e9c283dfde9867a5329e9751c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 19:02:07 +0200 Subject: [PATCH 229/433] Prepare for using cargo-release --- .typos.toml | 7 +++++++ Cargo.lock | 4 ++-- Cargo.toml | 8 ++++++-- release-hook.sh | 8 ++++++++ rustlings-macros/Cargo.toml | 3 +++ 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .typos.toml create mode 100755 release-hook.sh diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..a74498ab --- /dev/null +++ b/.typos.toml @@ -0,0 +1,7 @@ +[files] +extend-exclude = [ + "CHANGELOG.md", +] + +[default.extend-words] +"ratatui" = "ratatui" diff --git a/Cargo.lock b/Cargo.lock index 23c4887a..788cbbbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,7 +679,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.0.0-beta.0" +version = "6.0.0-alpha.0" dependencies = [ "anyhow", "assert_cmd", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.0" +version = "6.0.0-alpha.0" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index 31e74568..5f22665a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.0" +version = "6.0.0-alpha.0" authors = [ "Liv ", "Mo Bitar ", @@ -41,6 +41,7 @@ include = [ "/info.toml", "/LICENSE", "/README.md", + "/solutions/", "/src/", ] @@ -52,7 +53,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros", version = "6.0.0-beta.0" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-alpha.0" } serde.workspace = true toml_edit.workspace = true which = "6.0.1" @@ -66,3 +67,6 @@ panic = "abort" [profile.dev] panic = "abort" + +[package.metadata.release] +pre-release-hook = ["./release-hook.sh"] diff --git a/release-hook.sh b/release-hook.sh new file mode 100755 index 00000000..3a2c5376 --- /dev/null +++ b/release-hook.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Error out if any command fails +set -e + +typos +cargo outdated -w --exit-code 1 +cargo test --workspace --all-targets diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index c9c1d2f7..f9aba662 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -14,3 +14,6 @@ proc-macro = true quote = "1.0.36" serde.workspace = true toml_edit.workspace = true + +[package.metadata.release] +verify = false From 5595e1c3975b71017b79771f3f0f85038e80d1e6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 19:32:10 +0200 Subject: [PATCH 230/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 788cbbbd..ca4d1315 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,7 +679,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.0.0-alpha.0" +version = "6.0.0-beta.1" dependencies = [ "anyhow", "assert_cmd", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-alpha.0" +version = "6.0.0-beta.1" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5f22665a..caa139e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-alpha.0" +version = "6.0.0-beta.1" authors = [ "Liv ", "Mo Bitar ", @@ -53,7 +53,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-alpha.0" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.1" } serde.workspace = true toml_edit.workspace = true which = "6.0.1" From a4e623ea94757d976ec38a5a771ea07e64e9105e Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 19:33:24 +0200 Subject: [PATCH 231/433] Fix releasing rustlings --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index caa139e3..ce3cf049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,4 @@ panic = "abort" [package.metadata.release] pre-release-hook = ["./release-hook.sh"] +verify = false From 8d45cdb09df7fbff117b91f500ccf82980732a47 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 19:54:03 +0200 Subject: [PATCH 232/433] Fix missing info.toml in the macros crate --- Cargo.toml | 1 - README.md | 4 +++- rustlings-macros/Cargo.toml | 7 ++++--- rustlings-macros/info.toml | 1 + rustlings-macros/src/lib.rs | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) create mode 120000 rustlings-macros/info.toml diff --git a/Cargo.toml b/Cargo.toml index ce3cf049..caa139e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,4 +70,3 @@ panic = "abort" [package.metadata.release] pre-release-hook = ["./release-hook.sh"] -verify = false diff --git a/README.md b/README.md index 2164269f..4b502902 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,10 @@ This'll also install _Cargo_, Rust's package/project manager. The following command will download and compile Rustlings: + + ```bash -cargo install rustlings --locked +cargo install rustlings@6.0.0-beta.2 --locked ``` ### Initialization diff --git a/rustlings-macros/Cargo.toml b/rustlings-macros/Cargo.toml index f9aba662..20d6776e 100644 --- a/rustlings-macros/Cargo.toml +++ b/rustlings-macros/Cargo.toml @@ -6,6 +6,10 @@ authors.workspace = true repository.workspace = true license.workspace = true edition.workspace = true +include = [ + "/src/", + "/info.toml", +] [lib] proc-macro = true @@ -14,6 +18,3 @@ proc-macro = true quote = "1.0.36" serde.workspace = true toml_edit.workspace = true - -[package.metadata.release] -verify = false diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml new file mode 120000 index 00000000..37952912 --- /dev/null +++ b/rustlings-macros/info.toml @@ -0,0 +1 @@ +../info.toml \ No newline at end of file diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index 0bf3dbdf..fc2bcf14 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -15,7 +15,7 @@ struct InfoFile { #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - let exercises = toml_edit::de::from_str::(include_str!("../../info.toml")) + let exercises = toml_edit::de::from_str::(include_str!("../info.toml")) .expect("Failed to parse `info.toml`") .exercises; From aaea5b490fabecbd1111a6ece6446fedfc9800f3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 19:54:59 +0200 Subject: [PATCH 233/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca4d1315..6faeea2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,7 +679,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.0.0-beta.1" +version = "6.0.0-beta.2" dependencies = [ "anyhow", "assert_cmd", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.1" +version = "6.0.0-beta.2" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index caa139e3..78ebb9c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.1" +version = "6.0.0-beta.2" authors = [ "Liv ", "Mo Bitar ", @@ -53,7 +53,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.1" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.2" } serde.workspace = true toml_edit.workspace = true which = "6.0.1" From 5920a58e83e1594d2a8e00ec39c82ab1d345eb3d Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 19:58:55 +0200 Subject: [PATCH 234/433] Include dev/Cargo.toml --- Cargo.toml | 12 +++++++----- dev-Cargo.toml | 1 + src/dev/check.rs | 2 +- src/dev/update.rs | 2 +- src/init.rs | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) create mode 120000 dev-Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 78ebb9c9..b3bdaedc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,12 +37,14 @@ keywords = [ "learning", ] include = [ - "/exercises/", - "/info.toml", - "/LICENSE", - "/README.md", - "/solutions/", "/src/", + "/exercises/", + "/solutions/", + "/info.toml", + # A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded. + "/dev-Cargo.toml", + "/README.md", + "/LICENSE", ] [dependencies] diff --git a/dev-Cargo.toml b/dev-Cargo.toml new file mode 120000 index 00000000..9230c2e9 --- /dev/null +++ b/dev-Cargo.toml @@ -0,0 +1 @@ +dev/Cargo.toml \ No newline at end of file diff --git a/src/dev/check.rs b/src/dev/check.rs index 564aa0a2..b6e6f31f 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -166,7 +166,7 @@ pub fn check() -> Result<()> { if DEBUG_PROFILE { check_cargo_toml( &info_file.exercises, - include_str!("../../dev/Cargo.toml"), + include_str!("../../dev-Cargo.toml"), b"../", )?; } else { diff --git a/src/dev/update.rs b/src/dev/update.rs index d2f20aae..fe7622c0 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -29,7 +29,7 @@ pub fn update() -> Result<()> { if DEBUG_PROFILE { update_cargo_toml( &info_file.exercises, - include_str!("../../dev/Cargo.toml"), + include_str!("../../dev-Cargo.toml"), b"../", "dev/Cargo.toml", ) diff --git a/src/init.rs b/src/init.rs index ce239ea9..8a9fb365 100644 --- a/src/init.rs +++ b/src/init.rs @@ -31,7 +31,7 @@ pub fn init() -> Result<()> { .init_exercises_dir(&info_file.exercises) .context("Failed to initialize the `rustlings/exercises` directory")?; - let current_cargo_toml = include_str!("../dev/Cargo.toml"); + let current_cargo_toml = include_str!("../dev-Cargo.toml"); // Skip the first line (comment). let newline_ind = current_cargo_toml .as_bytes() From 078142c43cce4d312bcc506df998600f8c0b8647 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 21:07:10 +0200 Subject: [PATCH 235/433] Update dev/Cargo.toml --- README.md | 2 +- dev/Cargo.toml | 6 +++--- release-hook.sh | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4b502902..ff111e8f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.2 --locked +cargo install rustlings@6.0.0-beta.3 --locked ``` ### Initialization diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 8da41a56..ad39debf 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -16,7 +16,7 @@ bin = [ { name = "if1", path = "../exercises/03_if/if1.rs" }, { name = "if2", path = "../exercises/03_if/if2.rs" }, { name = "if3", path = "../exercises/03_if/if3.rs" }, - { name = "quiz1", path = "../exercises/quiz1.rs" }, + { name = "quiz1", path = "../exercises/quizzes/quiz1.rs" }, { name = "primitive_types1", path = "../exercises/04_primitive_types/primitive_types1.rs" }, { name = "primitive_types2", path = "../exercises/04_primitive_types/primitive_types2.rs" }, { name = "primitive_types3", path = "../exercises/04_primitive_types/primitive_types3.rs" }, @@ -47,7 +47,7 @@ bin = [ { name = "hashmaps1", path = "../exercises/11_hashmaps/hashmaps1.rs" }, { name = "hashmaps2", path = "../exercises/11_hashmaps/hashmaps2.rs" }, { name = "hashmaps3", path = "../exercises/11_hashmaps/hashmaps3.rs" }, - { name = "quiz2", path = "../exercises/quiz2.rs" }, + { name = "quiz2", path = "../exercises/quizzes/quiz2.rs" }, { name = "options1", path = "../exercises/12_options/options1.rs" }, { name = "options2", path = "../exercises/12_options/options2.rs" }, { name = "options3", path = "../exercises/12_options/options3.rs" }, @@ -64,7 +64,7 @@ bin = [ { name = "traits3", path = "../exercises/15_traits/traits3.rs" }, { name = "traits4", path = "../exercises/15_traits/traits4.rs" }, { name = "traits5", path = "../exercises/15_traits/traits5.rs" }, - { name = "quiz3", path = "../exercises/quiz3.rs" }, + { name = "quiz3", path = "../exercises/quizzes/quiz3.rs" }, { name = "lifetimes1", path = "../exercises/16_lifetimes/lifetimes1.rs" }, { name = "lifetimes2", path = "../exercises/16_lifetimes/lifetimes2.rs" }, { name = "lifetimes3", path = "../exercises/16_lifetimes/lifetimes3.rs" }, diff --git a/release-hook.sh b/release-hook.sh index 3a2c5376..052832f2 100755 --- a/release-hook.sh +++ b/release-hook.sh @@ -3,6 +3,7 @@ # Error out if any command fails set -e +cargo run -- dev check typos cargo outdated -w --exit-code 1 cargo test --workspace --all-targets From 0d7b0361375fb73f537d672bbecc11181e19e8c9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 25 Apr 2024 21:07:41 +0200 Subject: [PATCH 236/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6faeea2a..5767267e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,7 +679,7 @@ dependencies = [ [[package]] name = "rustlings" -version = "6.0.0-beta.2" +version = "6.0.0-beta.3" dependencies = [ "anyhow", "assert_cmd", @@ -698,7 +698,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.2" +version = "6.0.0-beta.3" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index b3bdaedc..1bc26c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.2" +version = "6.0.0-beta.3" authors = [ "Liv ", "Mo Bitar ", @@ -55,7 +55,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.2" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.3" } serde.workspace = true toml_edit.workspace = true which = "6.0.1" From e230ffcf03075c64b5bff9570b53ed86605c742e Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 01:19:52 +0200 Subject: [PATCH 237/433] Update the contributing guide --- CONTRIBUTING.md | 147 ++++++++++++------------------------------------ 1 file changed, 37 insertions(+), 110 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fc7fb79..c6a2d176 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,129 +1,56 @@ -## Contributing to Rustlings +# Contributing to Rustlings -First off, thanks for taking the time to contribute!! ❤️ +First off, thanks for taking the time to contribute! ❤️ -### Quick Reference +## Quick Reference -I want to... +I want to … -_add an exercise! ➡️ [read this](#addex) and then [open a Pull Request](#prs)_ +- _report a bug!_ ➡️ [open an issue](#issues) +- _fix a bug!_ ➡️ [open a pull request](#pull-requests) +- _implement a new feature!_ ➡️ [open an issue to discuss it first, then a pull request](#issues) +- _add an exercise!_ ➡️ [read this](#adding-an-exercise) +- _update an outdated exercise!_ ➡️ [open a pull request](#pull-requests) -_update an outdated exercise! ➡️ [open a Pull Request](#prs)_ - -_report a bug! ➡️ [open an Issue](#issues)_ - -_fix a bug! ➡️ [open a Pull Request](#prs)_ - -_implement a new feature! ➡️ [open an Issue to discuss it first, then a Pull Request](#issues)_ - - -### Working on the source code - -`rustlings` is basically a glorified `rustc` wrapper. Therefore the source code -isn't really that complicated since the bulk of the work is done by `rustc`. - - -### Adding an exercise - -The first step is to add the exercise! Name the file `exercises/yourTopic/yourTopicN.rs`, make sure to -put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`. - -Next make sure it runs with `rustlings`. The exercise metadata is stored in `info.toml`, under the `exercises` array. The order of the `exercises` array determines the order the exercises are run by `rustlings verify` and `rustlings watch`. - -Add the metadata for your exercise in the correct order in the `exercises` array. If you are unsure of the correct ordering, add it at the bottom and ask in your pull request. The exercise metadata should contain the following: -```diff - ... -+ [[exercises]] -+ name = "yourTopicN" -+ path = "exercises/yourTopic/yourTopicN.rs" -+ mode = "compile" -+ hint = """ -+ Some kind of useful hint for your exercise.""" - ... -``` - -The `mode` attribute decides whether Rustlings will only compile your exercise, or compile and test it. If you have tests to verify in your exercise, choose `test`, otherwise `compile`. If you're working on a Clippy exercise, use `mode = "clippy"`. - -That's all! Feel free to put up a pull request. - - -### Issues +## Issues You can open an issue [here](https://github.com/rust-lang/rustlings/issues/new). If you're reporting a bug, please include the output of the following commands: -- `rustc --version` +- `cargo --version` - `rustlings --version` - `ls -la` - Your OS name and version - -### Pull Requests +## Pull Requests -Opening a pull request is as easy as forking the repository and committing your -changes. There's a couple of things to watch out for: +You are welcome to open a pull request, but unless it is small and trivial, **please open an issue to discuss your idea first** 🙏🏼 -#### Write correct commit messages +Opening a pull request is as easy as forking the repository and committing your changes. +If you need any help with it or face any Git related problems, don't hesitate to ask for help 🤗 -We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) -specification. -This means that you have to format your commit messages in a specific way. Say -you're working on adding a new exercise called `foobar1.rs`. You could write -the following commit message: +It may take time to review your pull request. +Please be patient 😇 -``` -feat: add foobar1.rs exercise +## Adding An Exercise + +- Name the file `exercises/yourTopic/yourTopicN.rs`. +- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`. +- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it. +- Add the [metadata for your exercise](#exercise-metadata) in the `info.toml` file. +- Make sure your exercise runs with `rustlings run yourTopicN`. +- [Open a pull request](#pull-requests). + +### Exercise Metadata + +The exercise metadata should contain the following: + +```toml +[[exercises]] +name = "yourTopicN" +dir = "yourTopic" +hint = """A useful (multi-line) hint for your exercise.""" ``` -If you're just fixing a bug, please use the `fix` type: - -``` -fix(verify): make sure verify doesn't self-destruct -``` - -The scope within the brackets is optional, but should be any of these: - -- `installation` (for the installation script) -- `cli` (for general CLI changes) -- `verify` (for the verification source file) -- `watch` (for the watch functionality source) -- `run` (for the run functionality source) -- `EXERCISENAME` (if you're changing a specific exercise, or set of exercises, - substitute them here) - -When the commit also happens to close an existing issue, link it in the message -body: - -``` -fix: update foobar - -closes #101029908 -``` - -If you're doing simple changes, like updating a book link, use `chore`: - -``` -chore: update exercise1.rs book link -``` - -If you're updating documentation, use `docs`: - -``` -docs: add more information to Readme -``` - -If, and only if, you're absolutely sure you want to make a breaking change -(please discuss this beforehand!), add an exclamation mark to the type and -explain the breaking change in the message body: - -``` -fix!: completely change verification - -BREAKING CHANGE: This has to be done because lorem ipsum dolor -``` - -#### Pull Request Workflow - -Once you open a Pull Request, it may be reviewed or labeled (or both) until -the maintainers accept your change. Please be patient, it may take some time -for this to happen! +If your exercise doesn't contain any test, add `test = false` to the exercise metadata. +But adding tests is recommended. From be4dfe8be036e451e8763444e4de9e7428b88f40 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 01:49:36 +0200 Subject: [PATCH 238/433] Add hint about updating Rust --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ff111e8f..4b473990 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ The following command will download and compile Rustlings: cargo install rustlings@6.0.0-beta.3 --locked ``` +If the installation failes, make sure you have the latest Rust version by running `rustup update`. +Otherwise, please [report an issue](https://github.com/rust-lang/rustlings/issues/new). + ### Initialization After installing Rustlings, run the following command to initialize the `rustlings/` directory: From b7289e59aae11b294706b219df614efc2d852b60 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 01:55:44 +0200 Subject: [PATCH 239/433] Put --locked in the troubleshooting section --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b473990..840391bf 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,18 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.3 --locked +cargo install rustlings@6.0.0-beta.3 ``` -If the installation failes, make sure you have the latest Rust version by running `rustup update`. -Otherwise, please [report an issue](https://github.com/rust-lang/rustlings/issues/new). +#### Troubleshooting + +If the installation fails… + + + +- Make sure you have the latest Rust version by running `rustup update`. +- Try adding the `--locked` flag: `cargo install rustlings --locked` +- Otherwise, please [report an issue](https://github.com/rust-lang/rustlings/issues/new). ### Initialization From e63e668d867caa96045c3ce99e876ddcf425418f Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 02:00:42 +0200 Subject: [PATCH 240/433] Use

--- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 840391bf..edff62fb 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,8 @@ The following command will download and compile Rustlings: cargo install rustlings@6.0.0-beta.3 ``` -#### Troubleshooting - -If the installation fails… +
+🐛 If the installation fails… @@ -45,6 +44,8 @@ If the installation fails… - Try adding the `--locked` flag: `cargo install rustlings --locked` - Otherwise, please [report an issue](https://github.com/rust-lang/rustlings/issues/new). +
+ ### Initialization After installing Rustlings, run the following command to initialize the `rustlings/` directory: From 29658673381f545a9d54d8b02f85f07ec269fc07 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 02:02:14 +0200 Subject: [PATCH 241/433] Add click to expand --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index edff62fb..3775e83c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ cargo install rustlings@6.0.0-beta.3 ```
-🐛 If the installation fails… +If the installation fails 🐛… (click to expand) From 74ae5066031d2f8ee848a9394737a0599f145bda Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 03:17:35 +0200 Subject: [PATCH 242/433] Update README --- README.md | 73 +++++++++++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 3775e83c..170b62c6 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,17 @@ cargo install rustlings@6.0.0-beta.3 ```
-If the installation fails 🐛… (click to expand) + + +**If the installation fails…** (_click to expand_) + + - Make sure you have the latest Rust version by running `rustup update`. -- Try adding the `--locked` flag: `cargo install rustlings --locked` -- Otherwise, please [report an issue](https://github.com/rust-lang/rustlings/issues/new). +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.3 --locked` +- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new).
@@ -63,45 +67,48 @@ rustlings ## Doing exercises -The exercises are sorted by topic and can be found in the subdirectory `rustlings/exercises/`. -For every topic there is an additional README file with some resources to get you started on the topic. -We really recommend that you have a look at them before you start. +The exercises are sorted by topic and can be found in the subdirectory `exercises/`. +For every topic, there is an additional `README.md` file with some resources to get you started on the topic. +We highly recommend that you have a look at them before you start 📚️ -The task is simple. Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! -Some exercises are also run as tests, but Rustlings handles them all the same. -To run the exercises in the recommended order, execute: +Some exercises contain tests that need to pass for the exercise to be done. + +### Watch Mode + +To run the exercises, launch Rustlings: ```bash rustlings ``` -This will try to verify the completion of every exercise in a predetermined order (what we think is best for newcomers). -It will also rerun automatically every time you change a file in the `exercises/` directory. +This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers). +It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory. -In case you want to go by your own order, or want to only verify a single exercise, you can run: +
+ -```bash -rustlings run EXERCISE_NAME -``` +**If detecting file changes in the `exercises/` directory fails…** (_click to expand_) -Or simply use the following command to run the next pending exercise in the course: + -```bash -rustlings run -``` +You can add the `--manual-run` flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. -In case you get stuck, you can run the following command to get a hint for your exercise: +Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). -```bash -rustlings hint EXERCISE_NAME -``` +
-You can also get the hint for the next pending exercise with the following command: +### Exercise List -```bash -rustlings hint -``` +In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` or `list` to open the interactive exercise list. + +The list allows you to… + +- See the status of all exercises (done or pending) +- `c`: Continue at another exercise (temporarelly skip some exercises or go back to a previous one) +- `r`: Reset the status and file of an exercise (you need to reload/reopen its file in your editor afterwards) + +See the footer of the list for all possible keys. ## Continuing On @@ -112,15 +119,7 @@ Continue practicing your Rust skills by building your own projects, contributing ## Uninstalling Rustlings -If you want to remove Rustlings from your system, there are two steps. - -1️⃣ Remove the `rustlings` directory that was created by `rustlings init`: - -```bash -rm -r rustlings -``` - -2️⃣ Run `cargo uninstall` to remove the `rustlings` binary: +If you want to remove Rustlings from your system, run the following command: ```bash cargo uninstall rustlings @@ -130,7 +129,7 @@ That's it! ## Contributing -See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md). +See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗 ## Contributors ✨ From 9664f4357c85b6f3384ee697f56f1c6b458b1f0b Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 03:20:34 +0200 Subject: [PATCH 243/433] Use HTML in the summary --- README.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 170b62c6..1c53e4e9 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,7 @@ cargo install rustlings@6.0.0-beta.3 ```
- - -**If the installation fails…** (_click to expand_) - - +If the installation fails… (click to expand) @@ -86,11 +82,7 @@ This will start the _watch mode_ which walks you through the exercises in a pred It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory.
- - -**If detecting file changes in the `exercises/` directory fails…** (_click to expand_) - - +If detecting file changes in the exercises/ directory fails… (click to expand) You can add the `--manual-run` flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. From 2f071c97b05a324cebe5fc91bd941b231597c7e1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 03:25:31 +0200 Subject: [PATCH 244/433] Update README.md --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1c53e4e9..3fd5d248 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ After installing Rustlings, run the following command to initialize the `rustlin rustlings init ``` -Now, go into the newly initialized directory and run Rustlings for further instructions on getting started with the exercises: +Now, go into the newly initialized directory and launch Rustlings for further instructions on getting started with the exercises: ```bash cd rustlings/ @@ -72,11 +72,7 @@ Some exercises contain tests that need to pass for the exercise to be done. ### Watch Mode -To run the exercises, launch Rustlings: - -```bash -rustlings -``` +After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`. This will start the _watch mode_ which walks you through the exercises in a predefined order (what we think is best for newcomers). It will rerun the current exercise automatically every time you change the exercise's file in the `exercises/` directory. From 37fcbeb596fa03e874aab9601e88425c95ccd598 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 03:29:05 +0200 Subject: [PATCH 245/433] Add indentation for details --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3fd5d248..b407f053 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,9 @@ It will rerun the current exercise automatically every time you change the exerc
If detecting file changes in the exercises/ directory fails… (click to expand) -You can add the `--manual-run` flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. - -Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). +> You can add the `--manual-run` flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. +> +> Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL).
From 0ce5d9d4d7a58316e1864d2ae1abf8f89cb20c76 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 03:39:38 +0200 Subject: [PATCH 246/433] Update README.md --- README.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b407f053..81585065 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ cargo install rustlings@6.0.0-beta.3 -- Make sure you have the latest Rust version by running `rustup update`. +- Make sure you have the latest Rust version by running `rustup update` - Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.3 --locked` -- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new). +- Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
@@ -80,7 +80,7 @@ It will rerun the current exercise automatically every time you change the exerc
If detecting file changes in the exercises/ directory fails… (click to expand) -> You can add the `--manual-run` flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. +> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. > > Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). @@ -93,8 +93,8 @@ In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l The list allows you to… - See the status of all exercises (done or pending) -- `c`: Continue at another exercise (temporarelly skip some exercises or go back to a previous one) -- `r`: Reset the status and file of an exercise (you need to reload/reopen its file in your editor afterwards) +- `c`: Continue at another exercise (temporarily skip some exercises or go back to a previous one) +- `r`: Reset status and file of an exercise (you need to _reload/reopen_ its file in your editor afterwards) See the footer of the list for all possible keys. @@ -113,8 +113,6 @@ If you want to remove Rustlings from your system, run the following command: cargo uninstall rustlings ``` -That's it! - ## Contributing See [CONTRIBUTING.md](https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md) 🔗 From dc5c72bc19951313e80f038961fb446bd6ea02f5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 26 Apr 2024 03:44:16 +0200 Subject: [PATCH 247/433] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81585065..4edbaeff 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,12 @@ Before installing Rustlings, you need to have _Rust installed_. Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust. This'll also install _Cargo_, Rust's package/project manager. -🐧 If you're on Linux, make sure you've installed `gcc` (for a linker). Deb: `sudo apt install build-essential gcc`. Dnf: `sudo dnf install gcc`. +> 🐧 If you're on Linux, make sure you've installed `gcc` (for a linker). +> +> Deb: `sudo apt install gcc`. +> Dnf: `sudo dnf install gcc`. -🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by typing `xcode-select --install`. +> 🍎 If you're on MacOS, make sure you've installed Xcode and its developer tools by running `xcode-select --install`. ### Installing Rustlings @@ -68,7 +71,7 @@ For every topic, there is an additional `README.md` file with some resources to We highly recommend that you have a look at them before you start 📚️ Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! -Some exercises contain tests that need to pass for the exercise to be done. +Some exercises contain tests that need to pass for the exercise to be done ✅ ### Watch Mode From c82c3673245ca11d455b067c97fadda4a8406cb9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:14:59 +0200 Subject: [PATCH 248/433] Respect the target-dir config and show tests' output --- Cargo.lock | 69 ++++----------------- Cargo.toml | 2 +- src/app_state.rs | 12 +++- src/cmd.rs | 70 ++++++++++++++++++++++ src/exercise.rs | 145 +++++++++++++++++---------------------------- src/main.rs | 29 +++++++-- src/run.rs | 2 +- src/watch/state.rs | 5 +- 8 files changed, 176 insertions(+), 158 deletions(-) create mode 100644 src/cmd.rs diff --git a/Cargo.lock b/Cargo.lock index 5767267e..f9b48bd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,16 +271,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "filetime" version = "0.2.23" @@ -333,15 +323,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -419,12 +400,6 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" - [[package]] name = "lock_api" version = "0.4.11" @@ -664,19 +639,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.5.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustlings" version = "6.0.0-beta.3" @@ -692,8 +654,8 @@ dependencies = [ "ratatui", "rustlings-macros", "serde", + "serde_json", "toml_edit", - "which", ] [[package]] @@ -752,6 +714,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -935,18 +908,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "which" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" -dependencies = [ - "either", - "home", - "rustix", - "winsafe", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1126,12 +1087,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 1bc26c90..a77c84f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,9 +56,9 @@ notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.3" } +serde_json = "1.0.116" serde.workspace = true toml_edit.workspace = true -which = "6.0.1" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/src/app_state.rs b/src/app_state.rs index 476b5a9c..b980bdba 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,7 +7,7 @@ use crossterm::{ use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, - path::Path, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -39,6 +39,7 @@ pub struct AppState { final_message: String, file_buf: Vec, official_exercises: bool, + target_dir: PathBuf, } impl AppState { @@ -90,6 +91,7 @@ impl AppState { pub fn new( exercise_infos: Vec, final_message: String, + target_dir: PathBuf, ) -> (Self, StateFileStatus) { let exercises = exercise_infos .into_iter() @@ -127,6 +129,7 @@ impl AppState { final_message, file_buf: Vec::with_capacity(2048), official_exercises: !Path::new("info.toml").exists(), + target_dir, }; let state_file_status = slf.update_from_file(); @@ -154,6 +157,11 @@ impl AppState { &self.exercises[self.current_exercise_ind] } + #[inline] + pub fn target_dir(&self) -> &Path { + &self.target_dir + } + pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { if ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); @@ -313,7 +321,7 @@ impl AppState { write!(writer, "Running {exercise} ... ")?; writer.flush()?; - let success = exercise.run(&mut output)?; + let success = exercise.run(&mut output, &self.target_dir)?; if !success { writeln!(writer, "{}\n", "FAILED".red())?; diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 00000000..28f21c55 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,70 @@ +use anyhow::{Context, Result}; +use std::{io::Read, path::Path, process::Command}; + +pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { + let (mut reader, writer) = os_pipe::pipe() + .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; + + let writer_clone = writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{description}`") + })?; + + let mut handle = cmd + .stdout(writer_clone) + .stderr(writer) + .spawn() + .with_context(|| format!("Failed to run the command `{description}`"))?; + + // Prevent pipe deadlock. + drop(cmd); + + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{description}`"))?; + + output.push(b'\n'); + + handle + .wait() + .with_context(|| format!("Failed to wait on the command `{description}` to exit")) + .map(|status| status.success()) +} + +pub struct CargoCmd<'a> { + pub subcommand: &'a str, + pub args: &'a [&'a str], + pub exercise_name: &'a str, + pub description: &'a str, + pub hide_warnings: bool, + pub target_dir: &'a Path, + pub output: &'a mut Vec, + pub dev: bool, +} + +impl<'a> CargoCmd<'a> { + pub fn run(&mut self) -> Result { + let mut cmd = Command::new("cargo"); + cmd.arg(self.subcommand); + + // A hack to make `cargo run` work when developing Rustlings. + if self.dev { + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg(self.target_dir); + } + + cmd.arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(self.exercise_name) + .args(self.args); + + if self.hide_warnings { + cmd.env("RUSTFLAGS", "-A warnings"); + } + + run_cmd(cmd, self.description, self.output) + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 50f360e9..23dae6f5 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,57 +1,21 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - io::{Read, Write}, - process::{Command, Stdio}, + io::Write, + path::{Path, PathBuf}, + process::Command, }; -use crate::{in_official_repo, terminal_link::TerminalFileLink, DEBUG_PROFILE}; +use crate::{ + cmd::{run_cmd, CargoCmd}, + in_official_repo, + terminal_link::TerminalFileLink, + DEBUG_PROFILE, +}; pub const OUTPUT_CAPACITY: usize = 1 << 14; -fn run_command( - mut cmd: Command, - cmd_description: &str, - output: &mut Vec, - stderr: bool, -) -> Result { - let (mut reader, writer) = os_pipe::pipe().with_context(|| { - format!("Failed to create a pipe to run the command `{cmd_description}``") - })?; - - let (stdout, stderr) = if stderr { - ( - Stdio::from(writer.try_clone().with_context(|| { - format!("Failed to clone the pipe writer for the command `{cmd_description}`") - })?), - Stdio::from(writer), - ) - } else { - (Stdio::from(writer), Stdio::null()) - }; - - let mut handle = cmd - .stdout(stdout) - .stderr(stderr) - .spawn() - .with_context(|| format!("Failed to run the command `{cmd_description}`"))?; - - // Prevent pipe deadlock. - drop(cmd); - - reader - .read_to_end(output) - .with_context(|| format!("Failed to read the output of the command `{cmd_description}`"))?; - - output.push(b'\n'); - - handle - .wait() - .with_context(|| format!("Failed to wait on the command `{cmd_description}` to exit")) - .map(|status| status.success()) -} - pub struct Exercise { pub dir: Option<&'static str>, // Exercise's unique name @@ -66,11 +30,16 @@ pub struct Exercise { } impl Exercise { - fn run_bin(&self, output: &mut Vec) -> Result { + fn run_bin(&self, output: &mut Vec, target_dir: &Path) -> Result { writeln!(output, "{}", "Output".underlined())?; - let bin_path = format!("target/debug/{}", self.name); - let success = run_command(Command::new(&bin_path), &bin_path, output, true)?; + let mut bin_path = + PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); + bin_path.push(target_dir); + bin_path.push("debug"); + bin_path.push(self.name); + + let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; if !success { writeln!( @@ -85,43 +54,23 @@ impl Exercise { Ok(success) } - fn cargo_cmd( - &self, - command: &str, - args: &[&str], - cmd_description: &str, - output: &mut Vec, - dev: bool, - stderr: bool, - ) -> Result { - let mut cmd = Command::new("cargo"); - cmd.arg(command); - - // A hack to make `cargo run` work when developing Rustlings. - if dev { - cmd.arg("--manifest-path") - .arg("dev/Cargo.toml") - .arg("--target-dir") - .arg("target"); - } - - cmd.arg("--color") - .arg("always") - .arg("-q") - .arg("--bin") - .arg(self.name) - .args(args); - - run_command(cmd, cmd_description, output, stderr) - } - - pub fn run(&self, output: &mut Vec) -> Result { + pub fn run(&self, output: &mut Vec, target_dir: &Path) -> Result { output.clear(); // Developing the official Rustlings. let dev = DEBUG_PROFILE && in_official_repo(); - let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev, true)?; + let build_success = CargoCmd { + subcommand: "build", + args: &[], + exercise_name: self.name, + description: "cargo build …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; if !build_success { return Ok(false); } @@ -134,19 +83,28 @@ impl Exercise { } else { &["--profile", "test"] }; - let clippy_success = - self.cargo_cmd("clippy", clippy_args, "cargo clippy …", output, dev, true)?; + let clippy_success = CargoCmd { + subcommand: "clippy", + args: clippy_args, + exercise_name: self.name, + description: "cargo clippy …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; if !clippy_success { return Ok(false); } if !self.test { - return self.run_bin(output); + return self.run_bin(output, target_dir); } - let test_success = self.cargo_cmd( - "test", - &[ + let test_success = CargoCmd { + subcommand: "test", + args: &[ "--", "--color", "always", @@ -154,14 +112,17 @@ impl Exercise { "--format", "pretty", ], - "cargo test …", + exercise_name: self.name, + description: "cargo test …", + // Hide warnings because they are shown by Clippy. + hide_warnings: true, + target_dir, output, dev, - // Hide warnings because they are shown by Clippy. - false, - )?; + } + .run()?; - let run_success = self.run_bin(output)?; + let run_success = self.run_bin(output, target_dir)?; Ok(test_success && run_success) } diff --git a/src/main.rs b/src/main.rs index 7a142fdb..b03aa520 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,16 +5,18 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; +use serde::Deserialize; use std::{ io::{self, BufRead, Write}, - path::Path, - process::exit, + path::{Path, PathBuf}, + process::{exit, Command, Stdio}, }; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; mod app_state; mod cargo_toml; +mod cmd; mod dev; mod embedded; mod exercise; @@ -75,6 +77,11 @@ enum Subcommands { Dev(DevCommands), } +#[derive(Deserialize)] +struct CargoMetadata { + target_directory: PathBuf, +} + fn in_official_repo() -> bool { Path::new("dev/rustlings-repo.txt").exists() } @@ -86,7 +93,20 @@ fn main() -> Result<()> { bail!("{OLD_METHOD_ERR}"); } - which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + let target_dir = serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output")? + .target_directory; match args.command { Some(Subcommands::Init) => { @@ -122,6 +142,7 @@ fn main() -> Result<()> { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), + target_dir, ); if let Some(welcome_message) = info_file.welcome_message { @@ -198,7 +219,7 @@ The new method doesn't include cloning the Rustlings' repository. Please follow the instructions in the README: https://github.com/rust-lang/rustlings#getting-started"; -const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. +const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` Did you already install Rust? Try running `cargo --version` to diagnose the problem."; diff --git a/src/run.rs b/src/run.rs index cbc9ad70..9b5ddd34 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,7 @@ use crate::{ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run(&mut output)?; + let success = exercise.run(&mut output, app_state.target_dir())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 40c01bfc..82b745a4 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -50,7 +50,10 @@ impl<'a> WatchState<'a> { pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; - let success = self.app_state.current_exercise().run(&mut self.output)?; + let success = self + .app_state + .current_exercise() + .run(&mut self.output, self.app_state.target_dir())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { From 2150d629b18b3ba2ccd05606e69dc8d171df1027 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:15:16 +0200 Subject: [PATCH 249/433] Use --show-output instead of --nocapture --- src/exercise.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/exercise.rs b/src/exercise.rs index 23dae6f5..b62958b9 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -108,7 +108,7 @@ impl Exercise { "--", "--color", "always", - "--nocapture", + "--show-output", "--format", "pretty", ], From cb7ce006b54089fd46f36304ac5a143680c15a2a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:17:10 +0200 Subject: [PATCH 250/433] Bump version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4edbaeff..9747690c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.3 +cargo install rustlings@6.0.0-beta.4 ```
@@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.3 - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.3 --locked` +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.4 --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
From 181c81f0165b71b5eda5e35c04849d9ac30d0261 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:17:24 +0200 Subject: [PATCH 251/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9b48bd0..e142dfc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,7 +641,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustlings" -version = "6.0.0-beta.3" +version = "6.0.0-beta.4" dependencies = [ "anyhow", "assert_cmd", @@ -660,7 +660,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.3" +version = "6.0.0-beta.4" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index a77c84f6..7e526080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.3" +version = "6.0.0-beta.4" authors = [ "Liv ", "Mo Bitar ", @@ -55,7 +55,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.3" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.4" } serde_json = "1.0.116" serde.workspace = true toml_edit.workspace = true From c3a92b1248201e4d09b21ba2ff0791b1375f83f0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:21:29 +0200 Subject: [PATCH 252/433] Update deps --- Cargo.lock | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e142dfc1..0a35aa2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,7 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] @@ -402,9 +402,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -516,15 +516,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -610,6 +610,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -867,9 +876,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "utf8parse" @@ -1080,9 +1089,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] From 12504b01e910cd9066a9d8a6c41896470033b0c2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:32:06 +0200 Subject: [PATCH 253/433] Disable unneeded features in deps --- Cargo.lock | 1 - Cargo.toml | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a35aa2d..6a47c314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,7 +474,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" dependencies = [ - "crossbeam-channel", "log", "notify", ] diff --git a/Cargo.toml b/Cargo.toml index 7e526080..38ebb391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ toml_edit = { version = "0.22.12", default-features = false, features = ["parse" [package] name = "rustlings" description = "Small exercises to get you used to reading and writing Rust code!" -default-run = "rustlings" version.workspace = true authors.workspace = true repository.workspace = true @@ -52,9 +51,9 @@ anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.3" -notify-debouncer-mini = "0.4.1" +notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" -ratatui = "0.26.2" +ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.4" } serde_json = "1.0.116" serde.workspace = true From cdeb8ce2292d5968e3866fd96cc422756f5a0ff4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 17:31:51 +0200 Subject: [PATCH 254/433] Fix initialization --- src/app_state.rs | 32 +++++++++++++++++++++++++++++--- src/main.rs | 32 +++----------------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index b980bdba..7683c142 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -4,6 +4,7 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; +use serde::Deserialize; use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, @@ -32,6 +33,11 @@ pub enum StateFileStatus { NotRead, } +#[derive(Deserialize)] +struct CargoMetadata { + target_directory: PathBuf, +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -91,8 +97,24 @@ impl AppState { pub fn new( exercise_infos: Vec, final_message: String, - target_dir: PathBuf, - ) -> (Self, StateFileStatus) { + ) -> Result<(Self, StateFileStatus)> { + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + let target_dir = serde_json::de::from_slice::(&metadata_output) + .context( + "Failed to read the field `target_directory` from the `cargo metadata` output", + )? + .target_directory; + let exercises = exercise_infos .into_iter() .map(|mut exercise_info| { @@ -134,7 +156,7 @@ impl AppState { let state_file_status = slf.update_from_file(); - (slf, state_file_status) + Ok((slf, state_file_status)) } #[inline] @@ -388,6 +410,10 @@ impl AppState { } } +const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` +Did you already install Rust? +Try running `cargo --version` to diagnose the problem."; + const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" All exercises seem to be done. Recompiling and running all exercises to make sure that all of them are actually done. diff --git a/src/main.rs b/src/main.rs index b03aa520..bb70a757 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,11 +5,10 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; -use serde::Deserialize; use std::{ io::{self, BufRead, Write}, - path::{Path, PathBuf}, - process::{exit, Command, Stdio}, + path::Path, + process::exit, }; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; @@ -77,11 +76,6 @@ enum Subcommands { Dev(DevCommands), } -#[derive(Deserialize)] -struct CargoMetadata { - target_directory: PathBuf, -} - fn in_official_repo() -> bool { Path::new("dev/rustlings-repo.txt").exists() } @@ -93,21 +87,6 @@ fn main() -> Result<()> { bail!("{OLD_METHOD_ERR}"); } - let metadata_output = Command::new("cargo") - .arg("metadata") - .arg("-q") - .arg("--format-version") - .arg("1") - .arg("--no-deps") - .stdin(Stdio::null()) - .stderr(Stdio::inherit()) - .output() - .context(CARGO_METADATA_ERR)? - .stdout; - let target_dir = serde_json::de::from_slice::(&metadata_output) - .context("Failed to read the field `target_directory` from the `cargo metadata` output")? - .target_directory; - match args.command { Some(Subcommands::Init) => { if DEBUG_PROFILE { @@ -142,8 +121,7 @@ fn main() -> Result<()> { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), - target_dir, - ); + )?; if let Some(welcome_message) = info_file.welcome_message { match state_file_status { @@ -219,10 +197,6 @@ The new method doesn't include cloning the Rustlings' repository. Please follow the instructions in the README: https://github.com/rust-lang/rustlings#getting-started"; -const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` -Did you already install Rust? -Try running `cargo --version` to diagnose the problem."; - const FORMAT_VERSION_HIGHER_ERR: &str = "The format version specified in the `info.toml` file is higher than the last one supported. It is possible that you have an outdated version of Rustlings. From 016e6a014ee77d13ce5d7fc4e1a140276143c4b2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 17:32:42 +0200 Subject: [PATCH 255/433] Update serde --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a47c314..6cf6a305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -704,18 +704,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 38ebb391..e430cf13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ license = "MIT" edition = "2021" [workspace.dependencies] -serde = { version = "1.0.198", features = ["derive"] } +serde = { version = "1.0.199", features = ["derive"] } toml_edit = { version = "0.22.12", default-features = false, features = ["parse", "serde"] } [package] From edea76b5b9f2086b2a885c119cc56b04753f7eb5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 17:34:39 +0200 Subject: [PATCH 256/433] Bump version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9747690c..2e513dcb 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.4 +cargo install rustlings@6.0.0-beta.5 ```
@@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.4 - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.4 --locked` +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.5 --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
From 89e0f642791cf9e66143c0904501333cefbb8a61 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 17:35:08 +0200 Subject: [PATCH 257/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cf6a305..1c5463ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -649,7 +649,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustlings" -version = "6.0.0-beta.4" +version = "6.0.0-beta.5" dependencies = [ "anyhow", "assert_cmd", @@ -668,7 +668,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.4" +version = "6.0.0-beta.5" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index e430cf13..41ad49c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.4" +version = "6.0.0-beta.5" authors = [ "Liv ", "Mo Bitar ", @@ -54,7 +54,7 @@ hashbrown = "0.14.3" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.4" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.5" } serde_json = "1.0.116" serde.workspace = true toml_edit.workspace = true From 5658998c0c27d88e2129e52fec0aba5cacf26c30 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:24:09 +0200 Subject: [PATCH 258/433] Update welcome and final messages --- info.toml | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/info.toml b/info.toml index 14944728..a0248b5d 100644 --- a/info.toml +++ b/info.toml @@ -1,32 +1,27 @@ format_version = 1 -welcome_message = """Is this your first time? Don't worry, Rustlings was made for beginners! We are -going to teach you a lot of things about Rust, but before we can get -started, here's a couple of notes about how Rustlings operates: +welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners! +We are going to teach you a lot of things about Rust, but before we can +get started, here are some notes about how Rustlings operates: 1. The central concept behind Rustlings is that you solve exercises. These - exercises usually have some sort of syntax error in them, which will cause - them to fail compilation or testing. Sometimes there's a logic error instead - of a syntax error. No matter what error, it's your job to find it and fix it! - You'll know when you fixed it because then, the exercise will compile and - Rustlings will be able to move on to the next exercise. -2. If you run Rustlings in watch mode (which we recommend), it'll automatically - start with the first exercise. Don't get confused by an error message popping - up as soon as you run Rustlings! This is part of the exercise that you're - supposed to solve, so open the exercise file in an editor and start your - detective work! -3. If you're stuck on an exercise, there is a helpful hint you can view by typing - 'hint' (in watch mode), or running `rustlings hint exercise_name`. + exercises usually contain some syntax or logic errors which cause the + exercise to fail compilation or testing. It's your job to find all errors + and fix them! +2. Make sure to have your editor open in the `rustlings/` directory. Rustlings + will show you the path of the current exercise under the progress bar. Open + the exercise file in your editor, fix errors and save the file. Rustlings will + automatically detect the file change and rerun the exercise. If all errors are + fixed, Rustlings will ask you to move on to the next exercise. +3. If you're stuck on an exercise, you can request a hint by typing `h` or `hint` + followed by ENTER. 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings/issues/new). We look at every issue, - and sometimes, other learners do too so you can help each other out! - -Got all that? Great! To get started, run `rustlings watch` in order to get the first exercise. -Make sure to have your editor open in the `rustlings` directory! + (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, + other learners do too so you can help each other out! """ final_message = """We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, please don't hesitate to report them to our repo. +If you noticed any issues, don't hesitate to report them on Github. You can also contribute your own exercises to help the greater community! Before reporting an issue or contributing, please read our guidelines: From de0befef9c780f9539458b21582f39f843d0bbc3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:37:17 +0200 Subject: [PATCH 259/433] Update intro1 --- exercises/00_intro/intro1.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 70000392..62bf95f1 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,11 +1,10 @@ // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel -// ready for the next exercise, remove the `I AM NOT DONE` comment below. +// ready for the next exercise, enter `n` (or `next`) in the terminal. // -// If you're running this using `rustlings watch`: The exercise file will be -// reloaded when you change one of the lines below! Try adding a `println!` -// line, or try changing what it outputs in your terminal. Try removing a -// semicolon and see what happens! +// The exercise file will be reloaded when you change one of the lines below! +// Try adding a new `println!`. +// Try removing a semicolon and see what happens in the terminal! fn main() { println!("Hello and"); @@ -22,5 +21,6 @@ fn main() { println!("solve the exercises. Good luck!"); println!(); println!("The file of this exercise is `exercises/00_intro/intro1.rs`. Have a look!"); - println!("The current exercise path is shown under the progress bar in the watch mode."); + println!("The current exercise path will be always shown under the progress bar."); + println!("You can click on the path to open the exercise file in your editor."); } From 62a2c1a6d9e88c1f70bd9cbf2c174318eaac4677 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:37:44 +0200 Subject: [PATCH 260/433] Put long version in () --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2e513dcb..1c145b93 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ It will rerun the current exercise automatically every time you change the exerc
If detecting file changes in the exercises/ directory fails… (click to expand) -> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` or `run` in the watch mode. +> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` (or `run`) in the watch mode. > > Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). @@ -91,7 +91,7 @@ It will rerun the current exercise automatically every time you change the exerc ### Exercise List -In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` or `list` to open the interactive exercise list. +In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` (or `list`) to open the interactive exercise list. The list allows you to… From ee2b772dd5f84168d12e55c45330c777092948a6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:38:05 +0200 Subject: [PATCH 261/433] Update intro1 hint --- info.toml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/info.toml b/info.toml index a0248b5d..4204f274 100644 --- a/info.toml +++ b/info.toml @@ -5,7 +5,7 @@ We are going to teach you a lot of things about Rust, but before we can get started, here are some notes about how Rustlings operates: 1. The central concept behind Rustlings is that you solve exercises. These - exercises usually contain some syntax or logic errors which cause the + exercises usually contain some compiler or logic errors which cause the exercise to fail compilation or testing. It's your job to find all errors and fix them! 2. Make sure to have your editor open in the `rustlings/` directory. Rustlings @@ -13,8 +13,7 @@ get started, here are some notes about how Rustlings operates: the exercise file in your editor, fix errors and save the file. Rustlings will automatically detect the file change and rerun the exercise. If all errors are fixed, Rustlings will ask you to move on to the next exercise. -3. If you're stuck on an exercise, you can request a hint by typing `h` or `hint` - followed by ENTER. +3. If you're stuck on an exercise, enter `h` (or `hint`) to show a hint. 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, other learners do too so you can help each other out! @@ -36,9 +35,7 @@ name = "intro1" dir = "00_intro" test = false # TODO: Fix hint -hint = """ -Remove the `I AM NOT DONE` comment in the `exercises/intro00/intro1.rs` file -to move on to the next exercise.""" +hint = """Enter `n` (or `next`) followed by ENTER to move on to the next exercise""" [[exercises]] name = "intro2" From ea40804371d073730e7b5f6258d2a825c544c0b1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:38:26 +0200 Subject: [PATCH 262/433] Put long version in () --- src/watch.rs | 2 +- src/watch/state.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 5c3f1709..453d9a43 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -124,5 +124,5 @@ The automatic detection of exercise file changes failed :( Please try running `rustlings` again. If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. -You need to manually trigger running the current exercise using `r` or `run` then. +You need to manually trigger running the current exercise using `r` (or `run`) then. "; diff --git a/src/watch/state.rs b/src/watch/state.rs index 82b745a4..e5364c30 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -130,7 +130,7 @@ impl<'a> WatchState<'a> { self.writer, "{}\n", "Exercise done ✓ -When you are done experimenting, enter `n` or `next` to go to the next exercise 🦀" +When you are done experimenting, enter `n` (or `next`) to move on to the next exercise 🦀" .bold() .green(), )?; From c45d2c3255b6b606debfd16ef135f64f31f604d7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:38:38 +0200 Subject: [PATCH 263/433] Remove the I AM NOT DONE check --- tests/integration_tests.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index f81cc94b..7d30467b 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,5 +1,4 @@ use assert_cmd::prelude::*; -use predicates::boolean::PredicateBooleanExt; use std::process::Command; #[test] @@ -110,8 +109,7 @@ fn run_compile_exercise_does_not_prompt() { .args(["run", "pending_exercise"]) .current_dir("tests/fixture/state") .assert() - .code(0) - .stdout(predicates::str::contains("I AM NOT DONE").not()); + .code(0); } #[test] @@ -121,8 +119,7 @@ fn run_test_exercise_does_not_prompt() { .args(["run", "pending_test_exercise"]) .current_dir("tests/fixture/state") .assert() - .code(0) - .stdout(predicates::str::contains("I AM NOT DONE").not()); + .code(0); } #[test] From 75e2804c8369f2414318f58573444fa8e49d03f2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:42:09 +0200 Subject: [PATCH 264/433] Esacpe the list with ESC --- src/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index 790c02fe..40a069ab 100644 --- a/src/list.rs +++ b/src/list.rs @@ -42,7 +42,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> { ui_state.message.clear(); match key.code { - KeyCode::Char('q') => break, + KeyCode::Esc | KeyCode::Char('q') => break, KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(), KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), From aedeff8b243bad9205b84a657789b59928bf6524 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 23:45:26 +0200 Subject: [PATCH 265/433] Reorder the footer keys --- src/list/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list/state.rs b/src/list/state.rs index 19a77fe0..77f09367 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -194,7 +194,7 @@ impl<'a> UiState<'a> { let message = if self.message.is_empty() { // Help footer. Span::raw( - "↓/j ↑/k home/g end/G │ filter one/

ending │ eset │ ontinue at │ uit", + "↓/j ↑/k home/g end/G │ ontinue at │ eset │ filter one/

ending │ uit", ) } else { self.message.as_str().light_blue() From 1508938fed4e3800dcf45c807f67e87ebe8ca30b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 28 Apr 2024 23:21:13 +0200 Subject: [PATCH 266/433] Highlight the active filter --- src/list/state.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 77f09367..0f2a1c82 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use ratatui::{ layout::{Constraint, Rect}, style::{Style, Stylize}, - text::Span, + text::{Line, Span}, widgets::{Block, Borders, HighlightSpacing, Paragraph, Row, Table, TableState}, Frame, }; @@ -193,11 +193,25 @@ impl<'a> UiState<'a> { let message = if self.message.is_empty() { // Help footer. - Span::raw( - "↓/j ↑/k home/g end/G │ ontinue at │ eset │ filter one/

ending │ uit", - ) + let mut spans = Vec::with_capacity(4); + spans.push(Span::raw( + "↓/j ↑/k home/g end/G │ ontinue at │ eset │ filter ", + )); + match self.filter { + Filter::Done => { + spans.push("one".underlined().magenta()); + spans.push(Span::raw("/

ending")); + } + Filter::Pending => { + spans.push(Span::raw("one/")); + spans.push("

ending".underlined().magenta()); + } + Filter::None => spans.push(Span::raw("one/

ending")), + } + spans.push(Span::raw(" │ uit")); + Line::from(spans) } else { - self.message.as_str().light_blue() + Line::from(self.message.as_str().light_blue()) }; frame.render_widget( message, From 593f0e0916dab5d600d50208ba226786968026c3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 28 Apr 2024 23:22:11 +0200 Subject: [PATCH 267/433] Revert escaping with ESC in list to be able to clear the message --- src/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/list.rs b/src/list.rs index 40a069ab..790c02fe 100644 --- a/src/list.rs +++ b/src/list.rs @@ -42,7 +42,7 @@ pub fn list(app_state: &mut AppState) -> Result<()> { ui_state.message.clear(); match key.code { - KeyCode::Esc | KeyCode::Char('q') => break, + KeyCode::Char('q') => break, KeyCode::Down | KeyCode::Char('j') => ui_state.select_next(), KeyCode::Up | KeyCode::Char('k') => ui_state.select_previous(), KeyCode::Home | KeyCode::Char('g') => ui_state.select_first(), From 3c7e7368b20f7c5c4b3b561b9fef8e0182280878 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 28 Apr 2024 23:25:44 +0200 Subject: [PATCH 268/433] Add solutions to the initialized .gitignore --- src/init.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/init.rs b/src/init.rs index 8a9fb365..cb3a6bc6 100644 --- a/src/init.rs +++ b/src/init.rs @@ -69,6 +69,7 @@ pub fn init() -> Result<()> { } const GITIGNORE: &[u8] = b".rustlings-state.txt +solutions Cargo.lock target .vscode From 8c60ac267e4483b952407dd5875475242438b42e Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 29 Apr 2024 00:26:53 +0200 Subject: [PATCH 269/433] Add working environment section --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 1c145b93..52848fe5 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,21 @@ cd rustlings/ rustlings ``` +## Working environment + +### Editor + +Our general recommendation is [VS Code](https://code.visualstudio.com/) with the [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). +But any editor that supports [rust-analyzer](https://rust-analyzer.github.io/) should be enough for working on the exercises. + +### Terminal + +While working with Rustlings, please use a modern terminal for the best user experience. +The default terminal on Linux and Mac should be sufficient. +On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). + +If you use VS Code, the builtin terminal should also be fine. + ## Doing exercises The exercises are sorted by topic and can be found in the subdirectory `exercises/`. From 196d3c1a9820acc9c58134f62a08ec3671411777 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 29 Apr 2024 00:36:13 +0200 Subject: [PATCH 270/433] Bump version --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- README.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1c5463ff..597ee067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", diff --git a/Cargo.toml b/Cargo.toml index 41ad49c3..2054b770 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ include = [ anyhow = "1.0.82" clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" -hashbrown = "0.14.3" +hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } diff --git a/README.md b/README.md index 52848fe5..3ba080f5 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.5 +cargo install rustlings@6.0.0-beta.6 ```

@@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.5 - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.5 --locked` +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.6 --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
From 7f73219041fc6659d2e8e944c4e1d0341e323478 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 29 Apr 2024 00:36:50 +0200 Subject: [PATCH 271/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 597ee067..271d09a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -649,7 +649,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustlings" -version = "6.0.0-beta.5" +version = "6.0.0-beta.6" dependencies = [ "anyhow", "assert_cmd", @@ -668,7 +668,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.5" +version = "6.0.0-beta.6" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index 2054b770..efb15127 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.5" +version = "6.0.0-beta.6" authors = [ "Liv ", "Mo Bitar ", @@ -54,7 +54,7 @@ hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.5" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.6" } serde_json = "1.0.116" serde.workspace = true toml_edit.workspace = true From b6f40f2ec86abc70e7b8548996c948f6c5563f46 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 29 Apr 2024 17:01:47 +0200 Subject: [PATCH 272/433] Document main and app_state --- src/app_state.rs | 151 ++++++++++++++++++++++++++--------------------- src/dev.rs | 2 +- src/main.rs | 12 ++-- 3 files changed, 91 insertions(+), 74 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 7683c142..9d12c936 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -33,6 +33,7 @@ pub enum StateFileStatus { NotRead, } +// Parses parts of the output of `cargo metadata`. #[derive(Deserialize)] struct CargoMetadata { target_directory: PathBuf, @@ -41,14 +42,18 @@ struct CargoMetadata { pub struct AppState { current_exercise_ind: usize, exercises: Vec, + // Caches the number of done exercises to avoid iterating over all exercises every time. n_done: u16, final_message: String, + // Preallocated buffer for reading and writing the state file. file_buf: Vec, official_exercises: bool, + // Cargo's target directory. target_dir: PathBuf, } impl AppState { + // Update the app state from the state file. fn update_from_file(&mut self) -> StateFileStatus { self.file_buf.clear(); self.n_done = 0; @@ -98,6 +103,7 @@ impl AppState { exercise_infos: Vec, final_message: String, ) -> Result<(Self, StateFileStatus)> { + // Get the target directory from Cargo. let metadata_output = Command::new("cargo") .arg("metadata") .arg("-q") @@ -115,6 +121,7 @@ impl AppState { )? .target_directory; + // Build exercises from their metadata in the info file. let exercises = exercise_infos .into_iter() .map(|mut exercise_info| { @@ -184,6 +191,36 @@ impl AppState { &self.target_dir } + // Write the state file. + // The file's format is very simple: + // - The first line is a comment. + // - The second line is an empty line. + // - The third line is the name of the current exercise. It must end with `\n` even if there + // are no done exercises. + // - The fourth line is an empty line. + // - All remaining lines are the names of done exercises. + fn write(&mut self) -> Result<()> { + self.file_buf.clear(); + + self.file_buf + .extend_from_slice(b"DON'T EDIT THIS FILE!\n\n"); + self.file_buf + .extend_from_slice(self.current_exercise().name.as_bytes()); + self.file_buf.push(b'\n'); + + for exercise in &self.exercises { + if exercise.done { + self.file_buf.push(b'\n'); + self.file_buf.extend_from_slice(exercise.name.as_bytes()); + } + } + + fs::write(STATE_FILE_NAME, &self.file_buf) + .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; + + Ok(()) + } + pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { if ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); @@ -218,6 +255,8 @@ impl AppState { Ok(()) } + // Official exercises: Dump the original file from the binary. + // Third-party exercises: Reset the exercise file with `git stash`. fn reset(&self, ind: usize, dir_name: Option<&str>, path: &str) -> Result<()> { if self.official_exercises { return EMBEDDED_FILES @@ -271,6 +310,7 @@ impl AppState { Ok(exercise.path) } + // Return the index of the next pending exercise or `None` if all exercises are done. fn next_pending_exercise_ind(&self) -> Option { if self.current_exercise_ind == self.exercises.len() - 1 { // The last exercise is done. @@ -293,6 +333,8 @@ impl AppState { } } + // Official exercises: Dump the solution file form the binary and return its path. + // Third-party exercises: Check if a solution file exists and return its path in that case. pub fn current_solution_path(&self) -> Result> { if DEBUG_PROFILE { return Ok(None); @@ -328,6 +370,9 @@ impl AppState { } } + // Mark the current exercise as done and move on to the next pending exercise if one exists. + // If all exercises are marked as done, run all of them to make sure that they are actually + // done. If an exercise which is marked as done fails, mark it as pending and continue on it. pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { @@ -335,78 +380,48 @@ impl AppState { self.n_done += 1; } - let Some(ind) = self.next_pending_exercise_ind() else { - writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; + if let Some(ind) = self.next_pending_exercise_ind() { + self.set_current_exercise_ind(ind)?; - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - for (exercise_ind, exercise) in self.exercises().iter().enumerate() { - write!(writer, "Running {exercise} ... ")?; - writer.flush()?; - - let success = exercise.run(&mut output, &self.target_dir)?; - if !success { - writeln!(writer, "{}\n", "FAILED".red())?; - - self.current_exercise_ind = exercise_ind; - - // No check if the exercise is done before setting it to pending - // because no pending exercise was found. - self.exercises[exercise_ind].done = false; - self.n_done -= 1; - - self.write()?; - - return Ok(ExercisesProgress::Pending); - } - - writeln!(writer, "{}", "ok".green())?; - } - - writer.execute(Clear(ClearType::All))?; - writer.write_all(FENISH_LINE.as_bytes())?; - - let final_message = self.final_message.trim(); - if !final_message.is_empty() { - writer.write_all(final_message.as_bytes())?; - writer.write_all(b"\n")?; - } - - return Ok(ExercisesProgress::AllDone); - }; - - self.set_current_exercise_ind(ind)?; - - Ok(ExercisesProgress::Pending) - } - - // Write the state file. - // The file's format is very simple: - // - The first line is a comment. - // - The second line is an empty line. - // - The third line is the name of the current exercise. It must end with `\n` even if there - // are no done exercises. - // - The fourth line is an empty line. - // - All remaining lines are the names of done exercises. - fn write(&mut self) -> Result<()> { - self.file_buf.clear(); - - self.file_buf - .extend_from_slice(b"DON'T EDIT THIS FILE!\n\n"); - self.file_buf - .extend_from_slice(self.current_exercise().name.as_bytes()); - self.file_buf.push(b'\n'); - - for exercise in &self.exercises { - if exercise.done { - self.file_buf.push(b'\n'); - self.file_buf.extend_from_slice(exercise.name.as_bytes()); - } + return Ok(ExercisesProgress::Pending); } - fs::write(STATE_FILE_NAME, &self.file_buf) - .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; + writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; - Ok(()) + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + for (exercise_ind, exercise) in self.exercises().iter().enumerate() { + write!(writer, "Running {exercise} ... ")?; + writer.flush()?; + + let success = exercise.run(&mut output, &self.target_dir)?; + if !success { + writeln!(writer, "{}\n", "FAILED".red())?; + + self.current_exercise_ind = exercise_ind; + + // No check if the exercise is done before setting it to pending + // because no pending exercise was found. + self.exercises[exercise_ind].done = false; + self.n_done -= 1; + + self.write()?; + + return Ok(ExercisesProgress::Pending); + } + + writeln!(writer, "{}", "ok".green())?; + } + + writer.execute(Clear(ClearType::All))?; + writer.write_all(FENISH_LINE.as_bytes())?; + + let final_message = self.final_message.trim(); + if !final_message.is_empty() { + writer.write_all(final_message.as_bytes())?; + writer.write_all(b"\n")?; + } + + Ok(ExercisesProgress::AllDone) } } diff --git a/src/dev.rs b/src/dev.rs index 737de0de..107d4376 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -15,7 +15,7 @@ pub enum DevCommands { New { /// The path to create the project in path: PathBuf, - /// Don't initialize a Git repository in the project directory + /// Don't try to initialize a Git repository in the project directory #[arg(long)] no_git: bool, }, diff --git a/src/main.rs b/src/main.rs index bb70a757..c51f63ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,11 @@ const DEBUG_PROFILE: bool = { debug_profile }; +// The current directory is the official Rustligns repository. +fn in_official_repo() -> bool { + Path::new("dev/rustlings-repo.txt").exists() +} + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -54,7 +59,7 @@ struct Args { #[derive(Subcommand)] enum Subcommands { - /// Initialize Rustlings + /// Initialize the official Rustlings exercises Init, /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified Run { @@ -76,10 +81,6 @@ enum Subcommands { Dev(DevCommands), } -fn in_official_repo() -> bool { - Path::new("dev/rustlings-repo.txt").exists() -} - fn main() -> Result<()> { let args = Args::parse(); @@ -123,6 +124,7 @@ fn main() -> Result<()> { info_file.final_message.unwrap_or_default(), )?; + // Show the welcome message if the state file doesn't exist yet. if let Some(welcome_message) = info_file.welcome_message { match state_file_status { StateFileStatus::NotRead => { From fef66b80ad0b90d7bbc6ebe704f34816a4b3173a Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 01:39:31 +0200 Subject: [PATCH 273/433] Implement From for Exercise --- src/app_state.rs | 27 +-------------------------- src/exercise.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 9d12c936..6af10435 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -121,34 +121,9 @@ impl AppState { )? .target_directory; - // Build exercises from their metadata in the info file. let exercises = exercise_infos .into_iter() - .map(|mut exercise_info| { - // Leaking to be able to borrow in the watch mode `Table`. - // Leaking is not a problem because the `AppState` instance lives until - // the end of the program. - let path = exercise_info.path().leak(); - - exercise_info.name.shrink_to_fit(); - let name = exercise_info.name.leak(); - let dir = exercise_info.dir.map(|mut dir| { - dir.shrink_to_fit(); - &*dir.leak() - }); - - let hint = exercise_info.hint.trim().to_owned(); - - Exercise { - dir, - name, - path, - test: exercise_info.test, - strict_clippy: exercise_info.strict_clippy, - hint, - done: false, - } - }) + .map(Exercise::from) .collect::>(); let mut slf = Self { diff --git a/src/exercise.rs b/src/exercise.rs index b62958b9..37d33b7a 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -10,6 +10,7 @@ use std::{ use crate::{ cmd::{run_cmd, CargoCmd}, in_official_repo, + info_file::ExerciseInfo, terminal_link::TerminalFileLink, DEBUG_PROFILE, }; @@ -132,6 +133,34 @@ impl Exercise { } } +impl From for Exercise { + fn from(mut exercise_info: ExerciseInfo) -> Self { + // Leaking to be able to borrow in the watch mode `Table`. + // Leaking is not a problem because the `AppState` instance lives until + // the end of the program. + let path = exercise_info.path().leak(); + + exercise_info.name.shrink_to_fit(); + let name = exercise_info.name.leak(); + let dir = exercise_info.dir.map(|mut dir| { + dir.shrink_to_fit(); + &*dir.leak() + }); + + let hint = exercise_info.hint.trim().to_owned(); + + Exercise { + dir, + name, + path, + test: exercise_info.test, + strict_clippy: exercise_info.strict_clippy, + hint, + done: false, + } + } +} + impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { self.path.fmt(f) From 52c0f5b39efd7c71e63a3a680a1d91f3efc8eda5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 01:41:08 +0200 Subject: [PATCH 274/433] Fix clearing the terminal --- src/app_state.rs | 9 +++------ src/main.rs | 14 +++++++------- src/watch/state.rs | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 6af10435..907c1282 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -1,9 +1,5 @@ use anyhow::{bail, Context, Result}; -use crossterm::{ - style::Stylize, - terminal::{Clear, ClearType}, - ExecutableCommand, -}; +use crossterm::style::Stylize; use serde::Deserialize; use std::{ fs::{self, File}, @@ -13,6 +9,7 @@ use std::{ }; use crate::{ + clear_terminal, embedded::EMBEDDED_FILES, exercise::{Exercise, OUTPUT_CAPACITY}, info_file::ExerciseInfo, @@ -387,7 +384,7 @@ impl AppState { writeln!(writer, "{}", "ok".green())?; } - writer.execute(Clear(ClearType::All))?; + clear_terminal(writer)?; writer.write_all(FENISH_LINE.as_bytes())?; let final_message = self.final_message.trim(); diff --git a/src/main.rs b/src/main.rs index c51f63ca..3e37ce25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,8 @@ use anyhow::{bail, Context, Result}; use app_state::StateFileStatus; use clap::{Parser, Subcommand}; -use crossterm::{ - terminal::{Clear, ClearType}, - ExecutableCommand, -}; use std::{ - io::{self, BufRead, Write}, + io::{self, BufRead, StdoutLock, Write}, path::Path, process::exit, }; @@ -45,6 +41,10 @@ fn in_official_repo() -> bool { Path::new("dev/rustlings-repo.txt").exists() } +fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { + stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") +} + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -129,7 +129,7 @@ fn main() -> Result<()> { match state_file_status { StateFileStatus::NotRead => { let mut stdout = io::stdout().lock(); - stdout.execute(Clear(ClearType::All))?; + clear_terminal(&mut stdout)?; let welcome_message = welcome_message.trim(); write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; @@ -137,7 +137,7 @@ fn main() -> Result<()> { io::stdin().lock().read_until(b'\n', &mut Vec::new())?; - stdout.execute(Clear(ClearType::All))?; + clear_terminal(&mut stdout)?; } StateFileStatus::Read => (), } diff --git a/src/watch/state.rs b/src/watch/state.rs index e5364c30..2cf7521d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,13 +1,13 @@ use anyhow::Result; use crossterm::{ style::{style, Stylize}, - terminal::{size, Clear, ClearType}, - ExecutableCommand, + terminal::size, }; use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, + clear_terminal, exercise::OUTPUT_CAPACITY, progress_bar::progress_bar, terminal_link::TerminalFileLink, @@ -111,7 +111,7 @@ impl<'a> WatchState<'a> { // Prevent having the first line shifted. self.writer.write_all(b"\n")?; - self.writer.execute(Clear(ClearType::All))?; + clear_terminal(&mut self.writer)?; self.writer.write_all(&self.output)?; self.writer.write_all(b"\n")?; From 2b7ac915059a4baa2d9c86a583c73fc4f07a8775 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 01:46:57 +0200 Subject: [PATCH 275/433] Add press_enter_prompt --- src/main.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3e37ce25..15bcc8e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,11 @@ fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J") } +fn press_enter_prompt() -> io::Result<()> { + io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + Ok(()) +} + /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code #[derive(Parser)] #[command(version)] @@ -98,7 +103,7 @@ fn main() -> Result<()> { let mut stdout = io::stdout().lock(); stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; stdout.flush()?; - io::stdin().lock().read_until(b'\n', &mut Vec::new())?; + press_enter_prompt()?; stdout.write_all(b"\n")?; } @@ -134,9 +139,7 @@ fn main() -> Result<()> { let welcome_message = welcome_message.trim(); write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; stdout.flush()?; - - io::stdin().lock().read_until(b'\n', &mut Vec::new())?; - + press_enter_prompt()?; clear_terminal(&mut stdout)?; } StateFileStatus::Read => (), From 563727f47f06cf79bbb40c4b4e7fda67b65fb40f Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 02:14:20 +0200 Subject: [PATCH 276/433] test next_pending_exercise_ind --- src/app_state.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/app_state.rs b/src/app_state.rs index 907c1282..8cb3e466 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -428,3 +428,56 @@ const FENISH_LINE: &str = "+---------------------------------------------------- ▒▒ ▒▒ ▒▒ ▒▒\x1b[0m "; + +#[cfg(test)] +mod tests { + use super::*; + + fn dummy_exercise() -> Exercise { + Exercise { + dir: None, + name: "0", + path: "exercises/0.rs", + test: false, + strict_clippy: false, + hint: String::new(), + done: false, + } + } + + #[test] + fn next_pending_exercise() { + let mut app_state = AppState { + current_exercise_ind: 0, + exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], + n_done: 0, + final_message: String::new(), + file_buf: Vec::new(), + official_exercises: true, + target_dir: PathBuf::new(), + }; + + let mut assert = |done: [bool; 3], expected: [Option; 3]| { + for (exercise, done) in app_state.exercises.iter_mut().zip(done) { + exercise.done = done; + } + for (ind, expected) in expected.into_iter().enumerate() { + app_state.current_exercise_ind = ind; + assert_eq!( + app_state.next_pending_exercise_ind(), + expected, + "done={done:?}, ind={ind}", + ); + } + }; + + assert([true, true, true], [None, None, None]); + assert([false, false, false], [Some(1), Some(2), Some(0)]); + assert([false, true, true], [None, Some(0), Some(0)]); + assert([true, false, true], [Some(1), None, Some(1)]); + assert([true, true, false], [Some(2), Some(2), None]); + assert([true, false, false], [Some(1), Some(2), Some(1)]); + assert([false, true, false], [Some(2), Some(2), Some(0)]); + assert([false, false, true], [Some(1), Some(0), Some(0)]); + } +} From 3ae6c208b275dd17bb05f7fcdbb0090e40ba1325 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 02:43:51 +0200 Subject: [PATCH 277/433] Disable the pretty format because of `--show-output` --- src/exercise.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index 37d33b7a..4edf378e 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -105,14 +105,7 @@ impl Exercise { let test_success = CargoCmd { subcommand: "test", - args: &[ - "--", - "--color", - "always", - "--show-output", - "--format", - "pretty", - ], + args: &["--", "--color", "always", "--show-output"], exercise_name: self.name, description: "cargo test …", // Hide warnings because they are shown by Clippy. From 8e178ac60dd947dc4ae30126b9281106599ddbce Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 30 Apr 2024 02:48:56 +0200 Subject: [PATCH 278/433] Document and test cargo_toml --- src/cargo_toml.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 2345a7ee..106e6a7a 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -2,6 +2,10 @@ use anyhow::{Context, Result}; use crate::info_file::ExerciseInfo; +// Return the start and end index of the content of the list `bin = […]`. +// bin = [xxxxxxxxxxxxxxxxx] +// |start_ind | +// |end_ind pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { let start_ind = cargo_toml .find("bin = [") @@ -16,6 +20,8 @@ pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { Ok((start_ind, end_ind)) } +// Generate and append the content of the `bin` list in `Cargo.toml`. +// The `exercise_path_prefix` is the prefix of the `path` field of every list entry. pub fn append_bins( buf: &mut Vec, exercise_infos: &[ExerciseInfo], @@ -37,6 +43,7 @@ pub fn append_bins( } } +// Update the `bin` list and leave everything else unchanged. pub fn updated_cargo_toml( exercise_infos: &[ExerciseInfo], current_cargo_toml: &str, @@ -55,3 +62,61 @@ pub fn updated_cargo_toml( Ok(updated_cargo_toml) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bins_start_end_ind() { + assert_eq!(bins_start_end_ind("").ok(), None); + assert_eq!(bins_start_end_ind("[]").ok(), None); + assert_eq!(bins_start_end_ind("bin = [").ok(), None); + assert_eq!(bins_start_end_ind("bin = ]").ok(), None); + assert_eq!(bins_start_end_ind("bin = []").ok(), Some((7, 7))); + assert_eq!(bins_start_end_ind("bin= []").ok(), None); + assert_eq!(bins_start_end_ind("bin =[]").ok(), None); + assert_eq!(bins_start_end_ind("bin=[]").ok(), None); + assert_eq!(bins_start_end_ind("bin = [\nxxx\n]").ok(), Some((7, 12))); + } + + #[test] + fn test_bins() { + let exercise_infos = [ + ExerciseInfo { + name: String::from("1"), + dir: None, + test: true, + strict_clippy: true, + hint: String::new(), + }, + ExerciseInfo { + name: String::from("2"), + dir: Some(String::from("d")), + test: false, + strict_clippy: false, + hint: String::new(), + }, + ]; + + let mut buf = Vec::with_capacity(128); + append_bins(&mut buf, &exercise_infos, b""); + assert_eq!( + buf, + br#" + { name = "1", path = "exercises/1.rs" }, + { name = "2", path = "exercises/d/2.rs" }, +"#, + ); + + assert_eq!( + updated_cargo_toml(&exercise_infos, "abc\nbin = [xxx]\n123", b"../").unwrap(), + br#"abc +bin = [ + { name = "1", path = "../exercises/1.rs" }, + { name = "2", path = "../exercises/d/2.rs" }, +] +123"#, + ); + } +} From 32415e1e6cca9e0fb9a3019ed8e75956c7f7f92e Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 1 May 2024 17:55:49 +0200 Subject: [PATCH 279/433] Document cmd --- src/cmd.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cmd.rs b/src/cmd.rs index 28f21c55..e4bc1121 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,6 +1,8 @@ use anyhow::{Context, Result}; use std::{io::Read, path::Path, process::Command}; +// Run a command with a description for a possible error and append the merged stdout and stderr. +// The boolean in the returned `Result` is true if the command's exit status is success. pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { let (mut reader, writer) = os_pipe::pipe() .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; @@ -35,13 +37,18 @@ pub struct CargoCmd<'a> { pub args: &'a [&'a str], pub exercise_name: &'a str, pub description: &'a str, + // RUSTFLAGS="-A warnings" pub hide_warnings: bool, + // Added as `--target-dir` if `Self::dev` is true. pub target_dir: &'a Path, + // The output buffer to append the merged stdout and stderr. pub output: &'a mut Vec, + // true while developing Rustlings. pub dev: bool, } impl<'a> CargoCmd<'a> { + // Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. pub fn run(&mut self) -> Result { let mut cmd = Command::new("cargo"); cmd.arg(self.subcommand); From d425dbe203c17166e2e0b5692695448f0cb85513 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 1 May 2024 18:08:18 +0200 Subject: [PATCH 280/433] Test run_cmd --- src/cmd.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/cmd.rs b/src/cmd.rs index e4bc1121..9762cf85 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -75,3 +75,19 @@ impl<'a> CargoCmd<'a> { run_cmd(cmd, self.description, self.output) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_run_cmd() { + let mut cmd = Command::new("echo"); + cmd.arg("Hello"); + + let mut output = Vec::with_capacity(8); + run_cmd(cmd, "echo …", &mut output).unwrap(); + + assert_eq!(output, b"Hello\n\n"); + } +} From 74180ba1cccb69cecb94932795ebd8743fa42a6c Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 1 May 2024 19:16:59 +0200 Subject: [PATCH 281/433] Check for tests while test=false --- src/dev/check.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index b6e6f31f..143037c0 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -22,22 +22,17 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result Result Result Date: Wed, 1 May 2024 19:47:35 +0200 Subject: [PATCH 282/433] Document dev --- src/dev.rs | 3 +-- src/dev/check.rs | 15 ++++++++++----- src/dev/new.rs | 31 ++++++++++++++++--------------- src/dev/update.rs | 5 +++-- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index 107d4376..fada8b33 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -1,7 +1,6 @@ -use std::path::PathBuf; - use anyhow::{bail, Context, Result}; use clap::Subcommand; +use std::path::PathBuf; use crate::DEBUG_PROFILE; diff --git a/src/dev/check.rs b/src/dev/check.rs index 143037c0..81d05ce6 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -12,10 +12,12 @@ use crate::{ CURRENT_FORMAT_VERSION, DEBUG_PROFILE, }; +// Find a char that isn't allowed in the exercise's `name` or `dir`. fn forbidden_char(input: &str) -> Option { input.chars().find(|c| *c != '_' && !c.is_alphanumeric()) } +// Check the info of all exercises and return their paths in a set. fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); @@ -72,11 +74,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result Error { - anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display()) -} +// Check the `exercises` directory for unexpected files. +fn check_unexpected_files(info_file_paths: &hashbrown::HashSet) -> Result<()> { + fn unexpected_file(path: &Path) -> Error { + anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display()) + } -fn check_exercise_dir_files(info_file_paths: &hashbrown::HashSet) -> Result<()> { for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { let entry = entry.context("Failed to read the `exercises` directory")?; @@ -128,11 +131,12 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { } let info_file_paths = check_info_file_exercises(info_file)?; - check_exercise_dir_files(&info_file_paths)?; + check_unexpected_files(&info_file_paths)?; Ok(()) } +// Check that the Cargo.toml file is up-to-date. fn check_cargo_toml( exercise_infos: &[ExerciseInfo], current_cargo_toml: &str, @@ -159,6 +163,7 @@ pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; check_exercises(&info_file)?; + // A hack to make `cargo run -- dev check` work when developing Rustlings. if DEBUG_PROFILE { check_cargo_toml( &info_file.exercises, diff --git a/src/dev/new.rs b/src/dev/new.rs index 44487abf..fefc4fc1 100644 --- a/src/dev/new.rs +++ b/src/dev/new.rs @@ -8,6 +8,7 @@ use std::{ use crate::CURRENT_FORMAT_VERSION; +// Create a directory relative to the current directory and print its path. fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { create_dir(dir_name) .with_context(|| format!("Failed to create the directory {current_dir}/{dir_name}"))?; @@ -15,6 +16,7 @@ fn create_rel_dir(dir_name: &str, current_dir: &str) -> Result<()> { Ok(()) } +// Write a file relative to the current directory and print its path. fn write_rel_file(file_name: &str, current_dir: &str, content: C) -> Result<()> where C: AsRef<[u8]>, @@ -27,13 +29,13 @@ where } pub fn new(path: &Path, no_git: bool) -> Result<()> { - let dir_name = path.to_string_lossy(); + let dir_path_str = path.to_string_lossy(); - create_dir(path).with_context(|| format!("Failed to create the directory {dir_name}"))?; - println!("Created the directory {dir_name}"); + create_dir(path).with_context(|| format!("Failed to create the directory {dir_path_str}"))?; + println!("Created the directory {dir_path_str}"); set_current_dir(path) - .with_context(|| format!("Failed to set {dir_name} as the current directory"))?; + .with_context(|| format!("Failed to set {dir_path_str} as the current directory"))?; if !no_git && !Command::new("git") @@ -42,28 +44,28 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> { .context("Failed to run `git init`")? .success() { - bail!("`git init` didn't run successfully. See the error message above"); + bail!("`git init` didn't run successfully. See the possible error message above"); } - write_rel_file(".gitignore", &dir_name, GITIGNORE)?; + write_rel_file(".gitignore", &dir_path_str, GITIGNORE)?; - create_rel_dir("exercises", &dir_name)?; - create_rel_dir("solutions", &dir_name)?; + create_rel_dir("exercises", &dir_path_str)?; + create_rel_dir("solutions", &dir_path_str)?; write_rel_file( "info.toml", - &dir_name, + &dir_path_str, format!("{INFO_FILE_BEFORE_FORMAT_VERSION}{CURRENT_FORMAT_VERSION}{INFO_FILE_AFTER_FORMAT_VERSION}"), )?; - write_rel_file("Cargo.toml", &dir_name, CARGO_TOML)?; + write_rel_file("Cargo.toml", &dir_path_str, CARGO_TOML)?; - write_rel_file("README.md", &dir_name, README)?; + write_rel_file("README.md", &dir_path_str, README)?; - create_rel_dir(".vscode", &dir_name)?; + create_rel_dir(".vscode", &dir_path_str)?; write_rel_file( ".vscode/extensions.json", - &dir_name, + &dir_path_str, crate::init::VS_CODE_EXTENSIONS_JSON, )?; @@ -137,8 +139,7 @@ const README: &str = "# Rustlings 🦀 Welcome to these third-party Rustlings exercises 😃 -First, -[install Rustlings using the official instructions in the README of the Rustlings project](https://github.com/rust-lang/rustlings) ✅ +First, [install Rustlings using the official instructions in the README of the Rustlings project](https://github.com/rust-lang/rustlings) ✅ Then, open your terminal in this directory and run `rustlings` to get started with the exercises 🚀 "; diff --git a/src/dev/update.rs b/src/dev/update.rs index fe7622c0..66efe3d0 100644 --- a/src/dev/update.rs +++ b/src/dev/update.rs @@ -1,6 +1,5 @@ -use std::fs; - use anyhow::{Context, Result}; +use std::fs; use crate::{ cargo_toml::updated_cargo_toml, @@ -8,6 +7,7 @@ use crate::{ DEBUG_PROFILE, }; +// Update the `Cargo.toml` file. fn update_cargo_toml( exercise_infos: &[ExerciseInfo], current_cargo_toml: &str, @@ -26,6 +26,7 @@ fn update_cargo_toml( pub fn update() -> Result<()> { let info_file = InfoFile::parse()?; + // A hack to make `cargo run -- dev update` work when developing Rustlings. if DEBUG_PROFILE { update_cargo_toml( &info_file.exercises, From 2d0497bf3b6e9cf7edcf1a6d1899e0e5364fc49b Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 2 May 2024 17:08:39 +0200 Subject: [PATCH 283/433] Fix errors --- src/embedded.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index d7952a1f..a84e332b 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -25,9 +25,9 @@ impl WriteStrategy { .open(path), }; - file.context("Failed to open the file `{path}` in write mode")? + file.with_context(|| format!("Failed to open the file `{path}` in write mode"))? .write_all(content) - .context("Failed to write the file {path}") + .with_context(|| format!("Failed to write the file {path}")) } } From da9f97b0e0a54747202c43c6aed8346cc784cb02 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 12 May 2024 01:35:30 +0200 Subject: [PATCH 284/433] Update deps --- Cargo.lock | 91 +++++++++++++++++++++++++++++------------------------- Cargo.toml | 4 +-- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 271d09a4..7a6049ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,47 +31,48 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -79,9 +80,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "assert_cmd" @@ -100,9 +101,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" @@ -190,9 +191,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "compact_str" @@ -359,6 +360,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.12.1" @@ -396,9 +403,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "lock_api" @@ -480,9 +487,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -528,9 +535,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "predicates" @@ -564,9 +571,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -677,15 +684,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -704,18 +711,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.199" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -724,9 +731,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -824,9 +831,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" dependencies = [ "proc-macro2", "quote", @@ -1088,27 +1095,27 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index efb15127..bc10d028 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ include = [ ] [dependencies] -anyhow = "1.0.82" +anyhow = "1.0.83" clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" @@ -55,7 +55,7 @@ notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.6" } -serde_json = "1.0.116" +serde_json = "1.0.117" serde.workspace = true toml_edit.workspace = true From d9df809838191962a82e98ff01aaaa73950ba670 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 12 May 2024 17:40:53 +0200 Subject: [PATCH 285/433] Optimize embedded dirs --- rustlings-macros/src/lib.rs | 22 +++++++-- src/app_state.rs | 42 +++++++---------- src/embedded.rs | 90 ++++++++++++++++++++++++++----------- 3 files changed, 98 insertions(+), 56 deletions(-) diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index fc2bcf14..4417a4f9 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -25,14 +25,28 @@ pub fn include_files(_: TokenStream) -> TokenStream { let solution_files = exercises .iter() .map(|exercise| format!("../solutions/{}/{}.rs", exercise.dir, exercise.name)); - let dirs = exercises.iter().map(|exercise| &exercise.dir); - let readmes = exercises + + let mut dirs = Vec::with_capacity(32); + let mut dir_inds = vec![0; exercises.len()]; + + for (exercise, dir_ind) in exercises.iter().zip(&mut dir_inds) { + // The directory is often the last one inserted. + if let Some(ind) = dirs.iter().rev().position(|dir| *dir == exercise.dir) { + *dir_ind = dirs.len() - 1 - ind; + continue; + } + + dirs.push(exercise.dir.as_str()); + *dir_ind = dirs.len() - 1; + } + + let readmes = dirs .iter() - .map(|exercise| format!("../exercises/{}/README.md", exercise.dir)); + .map(|dir| format!("../exercises/{dir}/README.md")); quote! { EmbeddedFiles { - exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files) }),*], + exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*], exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*] } } diff --git a/src/app_state.rs b/src/app_state.rs index 8cb3e466..492be345 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -193,12 +193,12 @@ impl AppState { Ok(()) } - pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { - if ind >= self.exercises.len() { + pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> { + if exercise_ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); } - self.current_exercise_ind = ind; + self.current_exercise_ind = exercise_ind; self.write() } @@ -215,8 +215,11 @@ impl AppState { self.write() } - pub fn set_pending(&mut self, ind: usize) -> Result<()> { - let exercise = self.exercises.get_mut(ind).context(BAD_INDEX_ERR)?; + pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { + let exercise = self + .exercises + .get_mut(exercise_ind) + .context(BAD_INDEX_ERR)?; if exercise.done { exercise.done = false; @@ -229,16 +232,10 @@ impl AppState { // Official exercises: Dump the original file from the binary. // Third-party exercises: Reset the exercise file with `git stash`. - fn reset(&self, ind: usize, dir_name: Option<&str>, path: &str) -> Result<()> { + fn reset(&self, exercise_ind: usize, path: &str) -> Result<()> { if self.official_exercises { return EMBEDDED_FILES - .write_exercise_to_disk( - ind, - dir_name.context( - "Official exercises must be nested in the `exercises` directory", - )?, - path, - ) + .write_exercise_to_disk(exercise_ind, path) .with_context(|| format!("Failed to reset the exercise {path}")); } @@ -265,7 +262,7 @@ impl AppState { pub fn reset_current_exercise(&mut self) -> Result<&'static str> { self.set_pending(self.current_exercise_ind)?; let exercise = self.current_exercise(); - self.reset(self.current_exercise_ind, exercise.dir, exercise.path)?; + self.reset(self.current_exercise_ind, exercise.path)?; Ok(exercise.path) } @@ -277,7 +274,7 @@ impl AppState { self.set_pending(exercise_ind)?; let exercise = &self.exercises[exercise_ind]; - self.reset(exercise_ind, exercise.dir, exercise.path)?; + self.reset(exercise_ind, exercise.path)?; Ok(exercise.path) } @@ -315,18 +312,9 @@ impl AppState { let current_exercise = self.current_exercise(); if self.official_exercises { - let dir_name = current_exercise - .dir - .context("Official exercises must be nested in the `exercises` directory")?; - let solution_path = format!("solutions/{dir_name}/{}.rs", current_exercise.name); - - EMBEDDED_FILES.write_solution_to_disk( - self.current_exercise_ind, - dir_name, - &solution_path, - )?; - - Ok(Some(solution_path)) + EMBEDDED_FILES + .write_solution_to_disk(self.current_exercise_ind, current_exercise.name) + .map(Some) } else { let solution_path = if let Some(dir) = current_exercise.dir { format!("solutions/{dir}/{}.rs", current_exercise.name) diff --git a/src/embedded.rs b/src/embedded.rs index a84e332b..23c8d6e4 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Context, Error, Result}; +use anyhow::{Context, Error, Result}; use std::{ fs::{create_dir, create_dir_all, OpenOptions}, io::{self, Write}, @@ -34,6 +34,7 @@ impl WriteStrategy { struct ExerciseFiles { exercise: &'static [u8], solution: &'static [u8], + dir_ind: usize, } struct ExerciseDir { @@ -43,11 +44,10 @@ struct ExerciseDir { impl ExerciseDir { fn init_on_disk(&self) -> Result<()> { - let path_prefix = "exercises/"; - let readme_path_postfix = "/README.md"; - let mut dir_path = - String::with_capacity(path_prefix.len() + self.name.len() + readme_path_postfix.len()); - dir_path.push_str(path_prefix); + // 20 = 10 + 10 + // exercises/ + /README.md + let mut dir_path = String::with_capacity(20 + self.name.len()); + dir_path.push_str("exercises/"); dir_path.push_str(self.name); if let Err(e) = create_dir(&dir_path) { @@ -60,10 +60,9 @@ impl ExerciseDir { ); } - let readme_path = { - dir_path.push_str(readme_path_postfix); - dir_path - }; + let mut readme_path = dir_path; + readme_path.push_str("/README.md"); + WriteStrategy::Overwrite.write(&readme_path, self.readme)?; Ok(()) @@ -95,30 +94,71 @@ impl EmbeddedFiles { Ok(()) } - pub fn write_exercise_to_disk( - &self, - exercise_ind: usize, - dir_name: &str, - path: &str, - ) -> Result<()> { - let Some(dir) = self.exercise_dirs.iter().find(|dir| dir.name == dir_name) else { - bail!("`{dir_name}` not found in the embedded directories"); - }; + pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> { + let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind]; + let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind]; dir.init_on_disk()?; - WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].exercise) + WriteStrategy::Overwrite.write(path, exercise_files.exercise) } + // Write the solution file to disk and return its path. pub fn write_solution_to_disk( &self, exercise_ind: usize, - dir_name: &str, - path: &str, - ) -> Result<()> { - let dir_path = format!("solutions/{dir_name}"); + exercise_name: &str, + ) -> Result { + let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind]; + let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind]; + + // 14 = 10 + 1 + 3 + // solutions/ + / + .rs + let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); + dir_path.push_str("solutions/"); + dir_path.push_str(dir.name); create_dir_all(&dir_path) .with_context(|| format!("Failed to create the directory {dir_path}"))?; - WriteStrategy::Overwrite.write(path, self.exercise_files[exercise_ind].solution) + let mut solution_path = dir_path; + solution_path.push('/'); + solution_path.push_str(exercise_name); + solution_path.push_str(".rs"); + + WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?; + + Ok(solution_path) + } +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + + #[derive(Deserialize)] + struct ExerciseInfo { + dir: String, + } + + #[derive(Deserialize)] + struct InfoFile { + exercises: Vec, + } + + #[test] + fn dirs() { + let exercises = toml_edit::de::from_str::(include_str!("../info.toml")) + .expect("Failed to parse `info.toml`") + .exercises; + + assert_eq!(exercises.len(), EMBEDDED_FILES.exercise_files.len()); + + for (exercise, exercise_files) in exercises.iter().zip(EMBEDDED_FILES.exercise_files) { + assert_eq!( + exercise.dir, + EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind].name, + ); + } } } From 11fda5d70f568e0f528d91dd573447719abe05f4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 01:25:38 +0200 Subject: [PATCH 286/433] Move info.toml to rustlings-macros/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves the experience for contributors on Windows becuase Windows can't deal with git symbolic links out of the box… --- CONTRIBUTING.md | 2 +- Cargo.toml | 1 - info.toml | 1286 ---------------------------------- rustlings-macros/info.toml | 1287 ++++++++++++++++++++++++++++++++++- rustlings-macros/src/lib.rs | 4 +- src/embedded.rs | 3 +- src/info_file.rs | 4 +- 7 files changed, 1295 insertions(+), 1292 deletions(-) delete mode 100644 info.toml mode change 120000 => 100644 rustlings-macros/info.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6a2d176..bc00a6b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ Please be patient 😇 - Name the file `exercises/yourTopic/yourTopicN.rs`. - Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`. - Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it. -- Add the [metadata for your exercise](#exercise-metadata) in the `info.toml` file. +- Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file. - Make sure your exercise runs with `rustlings run yourTopicN`. - [Open a pull request](#pull-requests). diff --git a/Cargo.toml b/Cargo.toml index bc10d028..f2015cb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ include = [ "/src/", "/exercises/", "/solutions/", - "/info.toml", # A symlink to be able to include `dev/Cargo.toml` although `dev` is excluded. "/dev-Cargo.toml", "/README.md", diff --git a/info.toml b/info.toml deleted file mode 100644 index 4204f274..00000000 --- a/info.toml +++ /dev/null @@ -1,1286 +0,0 @@ -format_version = 1 - -welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners! -We are going to teach you a lot of things about Rust, but before we can -get started, here are some notes about how Rustlings operates: - -1. The central concept behind Rustlings is that you solve exercises. These - exercises usually contain some compiler or logic errors which cause the - exercise to fail compilation or testing. It's your job to find all errors - and fix them! -2. Make sure to have your editor open in the `rustlings/` directory. Rustlings - will show you the path of the current exercise under the progress bar. Open - the exercise file in your editor, fix errors and save the file. Rustlings will - automatically detect the file change and rerun the exercise. If all errors are - fixed, Rustlings will ask you to move on to the next exercise. -3. If you're stuck on an exercise, enter `h` (or `hint`) to show a hint. -4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! - (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, - other learners do too so you can help each other out! -""" - -final_message = """We hope you enjoyed learning about the various aspects of Rust! -If you noticed any issues, don't hesitate to report them on Github. -You can also contribute your own exercises to help the greater community! - -Before reporting an issue or contributing, please read our guidelines: -https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md -""" - -# INTRO - -# TODO: Update exercise -[[exercises]] -name = "intro1" -dir = "00_intro" -test = false -# TODO: Fix hint -hint = """Enter `n` (or `next`) followed by ENTER to move on to the next exercise""" - -[[exercises]] -name = "intro2" -dir = "00_intro" -test = false -hint = """ -The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" - -# VARIABLES - -[[exercises]] -name = "variables1" -dir = "01_variables" -test = false -hint = """ -The declaration in the first line in the main function is missing a keyword -that is needed in Rust to create a new variable binding.""" - -[[exercises]] -name = "variables2" -dir = "01_variables" -test = false -hint = """ -The compiler message is saying that Rust cannot infer the type that the -variable binding `x` has with what is given here. - -What happens if you annotate the first line in the main function with a type -annotation? - -What if you give `x` a value? - -What if you do both? - -What type should `x` be, anyway? - -What if `x` is the same type as `10`? What if it's a different type?""" - -[[exercises]] -name = "variables3" -dir = "01_variables" -test = false -hint = """ -Oops! In this exercise, we have a variable binding that we've created on in the -first line in the `main` function, and we're trying to use it in the next line, -but we haven't given it a value. - -We can't print out something that isn't there; try giving `x` a value! - -This is an error that can cause bugs that's very easy to make in any -programming language -- thankfully the Rust compiler has caught this for us!""" - -[[exercises]] -name = "variables4" -dir = "01_variables" -test = false -hint = """ -In Rust, variable bindings are immutable by default. But here we're trying -to reassign a different value to `x`! There's a keyword we can use to make -a variable binding mutable instead.""" - -[[exercises]] -name = "variables5" -dir = "01_variables" -test = false -hint = """ -In `variables4` we already learned how to make an immutable variable mutable -using a special keyword. Unfortunately this doesn't help us much in this -exercise because we want to assign a different typed value to an existing -variable. Sometimes you may also like to reuse existing variable names because -you are just converting values to different types like in this exercise. - -Fortunately Rust has a powerful solution to this problem: 'Shadowing'! -You can read more about 'Shadowing' in the book's section 'Variables and -Mutability': -https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing - -Try to solve this exercise afterwards using this technique.""" - -[[exercises]] -name = "variables6" -dir = "01_variables" -test = false -hint = """ -We know about variables and mutability, but there is another important type of -variable available: constants. - -Constants are always immutable and they are declared with keyword `const` rather -than keyword `let`. - -Constants types must also always be annotated. - -Read more about constants and the differences between variables and constants -under 'Constants' in the book's section 'Variables and Mutability': -https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants -""" - -# FUNCTIONS - -[[exercises]] -name = "functions1" -dir = "02_functions" -test = false -hint = """ -This main function is calling a function that it expects to exist, but the -function doesn't exist. It expects this function to have the name `call_me`. -It expects this function to not take any arguments and not return a value. -Sounds a lot like `main`, doesn't it?""" - -[[exercises]] -name = "functions2" -dir = "02_functions" -test = false -hint = """ -Rust requires that all parts of a function's signature have type annotations, -but `call_me` is missing the type annotation of `num`.""" - -[[exercises]] -name = "functions3" -dir = "02_functions" -test = false -hint = """ -This time, the function *declaration* is okay, but there's something wrong -with the place where we're calling the function.""" - -[[exercises]] -name = "functions4" -dir = "02_functions" -test = false -hint = """ -The error message points to the function `sale_price` and says it expects a type -after the `->`. This is where the function's return type should be -- take a -look at the `is_even` function for an example!""" - -[[exercises]] -name = "functions5" -dir = "02_functions" -test = false -hint = """ -This is a really common error that can be fixed by removing one character. -It happens because Rust distinguishes between expressions and statements: -expressions return a value based on their operand(s), and statements simply -return a `()` type which behaves just like `void` in C/C++ language. - -We want to return a value of `i32` type from the `square` function, but it is -returning a `()` type... - -They are not the same. There are two solutions: -1. Add a `return` ahead of `num * num;` -2. remove `;`, make it to be `num * num`""" - -# IF - -[[exercises]] -name = "if1" -dir = "03_if" -hint = """ -It's possible to do this in one line if you would like! - -Some similar examples from other languages: -- In C(++) this would be: `a > b ? a : b` -- In Python this would be: `a if a > b else b` - -Remember in Rust that: -- the `if` condition does not need to be surrounded by parentheses -- `if`/`else` conditionals are expressions -- Each condition is followed by a `{}` block.""" - -[[exercises]] -name = "if2" -dir = "03_if" -hint = """ -For that first compiler error, it's important in Rust that each conditional -block returns the same type! To get the tests passing, you will need a couple -conditions checking different input values.""" - -[[exercises]] -name = "if3" -dir = "03_if" -hint = """ -In Rust, every arm of an `if` expression has to return the same type of value. -Make sure the type is consistent across all arms.""" - -# QUIZ 1 - -[[exercises]] -name = "quiz1" -dir = "quizzes" -hint = "No hints this time ;)" - -# PRIMITIVE TYPES - -[[exercises]] -name = "primitive_types1" -dir = "04_primitive_types" -test = false -hint = "No hints this time ;)" - -[[exercises]] -name = "primitive_types2" -dir = "04_primitive_types" -test = false -hint = "No hints this time ;)" - -[[exercises]] -name = "primitive_types3" -dir = "04_primitive_types" -test = false -hint = """ -There's a shorthand to initialize Arrays with a certain size that does not -require you to type in 100 items (but you certainly can if you want!). - -For example, you can do: -``` -let array = ["Are we there yet?"; 10]; -``` - -Bonus: what are some other things you could have that would return `true` -for `a.len() >= 100`?""" - -[[exercises]] -name = "primitive_types4" -dir = "04_primitive_types" -hint = """ -Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section -of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use the -starting and ending (plus one) indices of the items in the `Array` that you -want to end up in the slice. - -If you're curious why the first argument of `assert_eq!` does not have an -ampersand for a reference since the second argument is a reference, take a look -at the coercion chapter of the nomicon: -https://doc.rust-lang.org/nomicon/coercions.html""" - -[[exercises]] -name = "primitive_types5" -dir = "04_primitive_types" -test = false -hint = """ -Take a look at the 'Data Types -> The Tuple Type' section of the book: -https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type -Particularly the part about destructuring (second to last example in the -section). - -You'll need to make a pattern to bind `name` and `age` to the appropriate parts -of the tuple. You can do it!!""" - -[[exercises]] -name = "primitive_types6" -dir = "04_primitive_types" -hint = """ -While you could use a destructuring `let` for the tuple here, try -indexing into it instead, as explained in the last example of the -'Data Types -> The Tuple Type' section of the book: -https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type -Now you have another tool in your toolbox!""" - -# VECS - -[[exercises]] -name = "vecs1" -dir = "05_vecs" -hint = """ -In Rust, there are two ways to define a Vector. -1. One way is to use the `Vec::new()` function to create a new vector - and fill it with the `push()` method. -2. The second way, which is simpler is to use the `vec![]` macro and - define your elements inside the square brackets. - -Check this chapter: https://doc.rust-lang.org/stable/book/ch08-01-vectors.html -of the Rust book to learn more. -""" - -[[exercises]] -name = "vecs2" -dir = "05_vecs" -hint = """ -In the first function we are looping over the Vector and getting a reference to -one `element` at a time. - -To modify the value of that `element` we need to use the `*` dereference -operator. You can learn more in this chapter of the Rust book: -https://doc.rust-lang.org/stable/book/ch08-01-vectors.html#iterating-over-the-values-in-a-vector - -In the second function this dereferencing is not necessary, because the `map` -function expects the new value to be returned. - -After you've completed both functions, decide for yourself which approach you -like better. - -What do you think is the more commonly used pattern under Rust developers? -""" - -# MOVE SEMANTICS - -[[exercises]] -name = "move_semantics1" -dir = "06_move_semantics" -hint = """ -So you've got the "cannot borrow immutable local variable `vec` as mutable" -error on the line where we push an element to the vector, right? - -The fix for this is going to be adding one keyword, and the addition is NOT on -the line where we push to the vector (where the error is). - -Also: Try accessing `vec0` after having called `fill_vec()`. See what -happens!""" - -[[exercises]] -name = "move_semantics2" -dir = "06_move_semantics" -hint = """ -When running this exercise for the first time, you'll notice an error about -"borrow of moved value". In Rust, when an argument is passed to a function and -it's not explicitly returned, you can't use the original variable anymore. -We call this "moving" a variable. When we pass `vec0` into `fill_vec`, it's -being "moved" into `vec1`, meaning we can't access `vec0` anymore after the -fact. - -Rust provides a couple of different ways to mitigate this issue, feel free to -try them all: -1. You could make another, separate version of the data that's in `vec0` and - pass that to `fill_vec` instead. -2. Make `fill_vec` borrow its argument instead of taking ownership of it, - and then copy the data within the function (`vec.clone()`) in order to - return an owned `Vec`. -""" - -[[exercises]] -name = "move_semantics3" -dir = "06_move_semantics" -hint = """ -The difference between this one and the previous ones is that the first line -of `fn fill_vec` that had `let mut vec = vec;` is no longer there. You can, -instead of adding that line back, add `mut` in one place that will change -an existing binding to be a mutable binding instead of an immutable one :)""" - -[[exercises]] -name = "move_semantics4" -dir = "06_move_semantics" -hint = """ -Stop reading whenever you feel like you have enough direction :) Or try -doing one step and then fixing the compiler errors that result! -So the end goal is to: - - get rid of the first line in main that creates the new vector - - so then `vec0` doesn't exist, so we can't pass it to `fill_vec` - - `fill_vec` has had its signature changed, which our call should reflect - - since we're not creating a new vec in `main` anymore, we need to create - a new vec in `fill_vec`, and fill it with the expected values""" - -[[exercises]] -name = "move_semantics5" -dir = "06_move_semantics" -hint = """ -Carefully reason about the range in which each mutable reference is in -scope. Does it help to update the value of referent (`x`) immediately after -the mutable reference is taken? Read more about 'Mutable References' -in the book's section 'References and Borrowing': -https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references. -""" - -[[exercises]] -name = "move_semantics6" -dir = "06_move_semantics" -test = false -hint = """ -To find the answer, you can consult the book section "References and Borrowing": -https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html - -The first problem is that `get_char` is taking ownership of the string. So -`data` is moved and can't be used for `string_uppercase`. `data` is moved to -`get_char` first, meaning that `string_uppercase` cannot manipulate the data. - -Once you've fixed that, `string_uppercase`'s function signature will also need -to be adjusted. - -Can you figure out how? - -Another hint: it has to do with the `&` character.""" - -# STRUCTS - -[[exercises]] -name = "structs1" -dir = "07_structs" -hint = """ -Rust has more than one type of struct. Three actually, all variants are used to -package related data together. - -There are normal (or classic) structs. These are named collections of related -data stored in fields. - -Tuple structs are basically just named tuples. - -Finally, Unit-like structs. These don't have any fields and are useful for -generics. - -In this exercise you need to complete and implement one of each kind. -Read more about structs in The Book: -https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" - -[[exercises]] -name = "structs2" -dir = "07_structs" -hint = """ -Creating instances of structs is easy, all you need to do is assign some values -to its fields. - -There are however some shortcuts that can be taken when instantiating structs. -Have a look in The Book, to find out more: -https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax""" - -[[exercises]] -name = "structs3" -dir = "07_structs" -hint = """ -For `is_international`: What makes a package international? Seems related to -the places it goes through right? - -For `get_fees`: This method takes an additional argument, is there a field in -the `Package` struct that this relates to? - -Have a look in The Book, to find out more about method implementations: -https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" - -# ENUMS - -[[exercises]] -name = "enums1" -dir = "08_enums" -test = false -hint = "No hints this time ;)" - -[[exercises]] -name = "enums2" -dir = "08_enums" -test = false -hint = """ -You can create enumerations that have different variants with different types -such as no data, anonymous structs, a single string, tuples, ...etc""" - -[[exercises]] -name = "enums3" -dir = "08_enums" -hint = """ -As a first step, you can define enums to compile this code without errors. - -And then create a match expression in `process()`. - -Note that you need to deconstruct some message variants in the match expression -to get value in the variant.""" - -# STRINGS - -[[exercises]] -name = "strings1" -dir = "09_strings" -test = false -hint = """ -The `current_favorite_color` function is currently returning a string slice -with the `'static` lifetime. We know this because the data of the string lives -in our code itself -- it doesn't come from a file or user input or another -program -- so it will live as long as our program lives. - -But it is still a string slice. There's one way to create a `String` by -converting a string slice covered in the Strings chapter of the book, and -another way that uses the `From` trait.""" - -[[exercises]] -name = "strings2" -dir = "09_strings" -test = false -hint = """ -Yes, it would be really easy to fix this by just changing the value bound to -`word` to be a string slice instead of a `String`, wouldn't it?? There is a way -to add one character to the `if` statement, though, that will coerce the -`String` into a string slice. - -Side note: If you're interested in learning about how this kind of reference -conversion works, you can jump ahead in the book and read this part in the -smart pointers chapter: -https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods""" - -[[exercises]] -name = "strings3" -dir = "09_strings" -hint = """ -There's tons of useful standard library functions for strings. Let's try and use some of them: -https://doc.rust-lang.org/std/string/struct.String.html#method.trim - -For the `compose_me` method: You can either use the `format!` macro, or convert -the string slice into an owned string, which you can then freely extend.""" - -[[exercises]] -name = "strings4" -dir = "09_strings" -test = false -hint = "No hints this time ;)" - -# MODULES - -[[exercises]] -name = "modules1" -dir = "10_modules" -test = false -hint = """ -Everything is private in Rust by default-- but there's a keyword we can use -to make something public! The compiler error should point to the thing that -needs to be public.""" - -[[exercises]] -name = "modules2" -dir = "10_modules" -test = false -hint = """ -The delicious_snacks module is trying to present an external interface that is -different than its internal structure (the `fruits` and `veggies` modules and -associated constants). Complete the `use` statements to fit the uses in main and -find the one keyword missing for both constants. - -Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use""" - -[[exercises]] -name = "modules3" -dir = "10_modules" -test = false -hint = """ -`UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a -`use` statement for these two to bring them into scope. You can use nested -paths or the glob operator to bring these two in using only one line.""" - -# HASHMAPS - -[[exercises]] -name = "hashmaps1" -dir = "11_hashmaps" -hint = """ -Hint 1: Take a look at the return type of the function to figure out - the type for the `basket`. - -Hint 2: Number of fruits should be at least 5. And you have to put - at least three different types of fruits. -""" - -[[exercises]] -name = "hashmaps2" -dir = "11_hashmaps" -hint = """ -Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. -Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value -""" - -[[exercises]] -name = "hashmaps3" -dir = "11_hashmaps" -hint = """ -Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert - entries corresponding to each team in the scores table. - -Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value - -Hint 2: If there is already an entry for a given key, the value returned by - `entry()` can be updated based on the existing value. - -Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-value-based-on-the-old-value -""" - -# QUIZ 2 - -[[exercises]] -name = "quiz2" -dir = "quizzes" -hint = "No hints this time ;)" - -# OPTIONS - -[[exercises]] -name = "options1" -dir = "12_options" -hint = """ -Options can have a `Some` value, with an inner value, or a `None` value, -without an inner value. - -There's multiple ways to get at the inner value, you can use `unwrap`, or -pattern match. Unwrapping is the easiest, but how do you do it safely so that -it doesn't panic in your face later?""" - -[[exercises]] -name = "options2" -dir = "12_options" -hint = """ -Check out: - -- https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html -- https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html - -Remember that `Option`s can be stacked in `if let` and `while let`. - -For example: `Some(Some(variable)) = variable2` - -Also see `Option::flatten` -""" - -[[exercises]] -name = "options3" -dir = "12_options" -test = false -hint = """ -The compiler says a partial move happened in the `match` statement. How can -this be avoided? The compiler shows the correction needed. - -After making the correction as suggested by the compiler, do read: -https://doc.rust-lang.org/std/keyword.ref.html""" - -# ERROR HANDLING - -[[exercises]] -name = "errors1" -dir = "13_error_handling" -hint = """ -`Ok` and `Err` are the two variants of `Result`, so what the tests are saying -is that `generate_nametag_text` should return a `Result` instead of an `Option`. - -To make this change, you'll need to: - - update the return type in the function signature to be a `Result` that could be the variants `Ok(String)` and `Err(String)` - - change the body of the function to return `Ok(stuff)` where it currently - returns `Some(stuff)` - - change the body of the function to return `Err(error message)` where it - currently returns `None`""" - -[[exercises]] -name = "errors2" -dir = "13_error_handling" -hint = """ -One way to handle this is using a `match` statement on -`item_quantity.parse::()` where the cases are `Ok(something)` and -`Err(something)`. - -This pattern is very common in Rust, though, so there's a `?` operator that -does pretty much what you would make that match statement do for you! - -Take a look at this section of the 'Error Handling' chapter: -https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator -and give it a try!""" - -[[exercises]] -name = "errors3" -dir = "13_error_handling" -test = false -hint = """ -If other functions can return a `Result`, why shouldn't `main`? It's a fairly -common convention to return something like `Result<(), ErrorType>` from your -main function. - -The unit (`()`) type is there because nothing is really needed in terms of -positive results.""" - -[[exercises]] -name = "errors4" -dir = "13_error_handling" -hint = """ -`PositiveNonzeroInteger::new` is always creating a new instance and returning -an `Ok` result. - -It should be doing some checking, returning an `Err` result if those checks -fail, and only returning an `Ok` result if those checks determine that -everything is... okay :)""" - -[[exercises]] -name = "errors5" -dir = "13_error_handling" -test = false -hint = """ -There are two different possible `Result` types produced within `main()`, which -are propagated using `?` operators. How do we declare a return type from -`main()` that allows both? - -Under the hood, the `?` operator calls `From::from` on the error value to -convert it to a boxed trait object, a `Box`. This boxed trait -object is polymorphic, and since all errors implement the `error::Error` trait, -we can capture lots of different errors in one "Box" object. - -Check out this section of the book: -https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator - -Read more about boxing errors: -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/boxing_errors.html - -Read more about using the `?` operator with boxed errors: -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html -""" - -[[exercises]] -name = "errors6" -dir = "13_error_handling" -hint = """ -This exercise uses a completed version of `PositiveNonzeroInteger` from -errors4. - -Below the line that `TODO` asks you to change, there is an example of using -the `map_err()` method on a `Result` to transform one type of error into -another. Try using something similar on the `Result` from `parse()`. You -might use the `?` operator to return early from the function, or you might -use a `match` expression, or maybe there's another way! - -You can create another function inside `impl ParsePosNonzeroError` to use -with `map_err()`. - -Read more about `map_err()` in the `std::result` documentation: -https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" - -# Generics - -[[exercises]] -name = "generics1" -dir = "14_generics" -test = false -hint = """ -Vectors in Rust make use of generics to create dynamically sized arrays of any -type. - -You need to tell the compiler what type we are pushing onto this vector.""" - -[[exercises]] -name = "generics2" -dir = "14_generics" -hint = """ -Currently we are wrapping only values of type `u32`. - -Maybe we could update the explicit references to this data type somehow? - -If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions -""" - -# TRAITS - -[[exercises]] -name = "traits1" -dir = "15_traits" -hint = """ -A discussion about Traits in Rust can be found at: -https://doc.rust-lang.org/book/ch10-02-traits.html -""" - -[[exercises]] -name = "traits2" -dir = "15_traits" -hint = """ -Notice how the trait takes ownership of `self`, and returns `Self`. - -Try mutating the incoming string vector. Have a look at the tests to see -what the result should look like! - -Vectors provide suitable methods for adding an element at the end. See -the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" - -[[exercises]] -name = "traits3" -dir = "15_traits" -hint = """ -Traits can have a default implementation for functions. Structs that implement -the trait can then use the default version of these functions if they choose not -to implement the function themselves. - -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations -""" - -[[exercises]] -name = "traits4" -dir = "15_traits" -hint = """ -Instead of using concrete types as parameters you can use traits. Try replacing -the '??' with 'impl ' - -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters -""" - -[[exercises]] -name = "traits5" -dir = "15_traits" -test = false -hint = """ -To ensure a parameter implements multiple traits use the '+ syntax'. Try -replacing the '??' with 'impl <> + <>'. - -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax -""" - -# QUIZ 3 - -[[exercises]] -name = "quiz3" -dir = "quizzes" -hint = """ -To find the best solution to this challenge you're going to need to think back -to your knowledge of traits, specifically 'Trait Bound Syntax' - -You may also need this: `use std::fmt::Display;`.""" - -# LIFETIMES - -[[exercises]] -name = "lifetimes1" -dir = "16_lifetimes" -test = false -hint = """ -Let the compiler guide you. Also take a look at the book if you need help: -https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" - -[[exercises]] -name = "lifetimes2" -dir = "16_lifetimes" -test = false -hint = """ -Remember that the generic lifetime `'a` will get the concrete lifetime that is -equal to the smaller of the lifetimes of `x` and `y`. - -You can take at least two paths to achieve the desired result while keeping the -inner block: -1. Move the `string2` declaration to make it live as long as `string1` (how is - `result` declared?) -2. Move `println!` into the inner block""" - -[[exercises]] -name = "lifetimes3" -dir = "16_lifetimes" -test = false -hint = """ -If you use a lifetime annotation in a struct's fields, where else does it need -to be added?""" - -# TESTS - -[[exercises]] -name = "tests1" -dir = "17_tests" -hint = """ -You don't even need to write any code to test -- you can just test values and -run that, even though you wouldn't do that in real life. :) - -`assert!` is a macro that needs an argument. Depending on the value of the -argument, `assert!` will do nothing (in which case the test will pass) or -`assert!` will panic (in which case the test will fail). - -So try giving different values to `assert!` and see which ones compile, which -ones pass, and which ones fail :)""" - -[[exercises]] -name = "tests2" -dir = "17_tests" -hint = """ -Like the previous exercise, you don't need to write any code to get this test -to compile and run. - -`assert_eq!` is a macro that takes two arguments and compares them. Try giving -it two values that are equal! Try giving it two arguments that are different! -Try giving it two values that are of different types! Try switching which -argument comes first and which comes second!""" - -[[exercises]] -name = "tests3" -dir = "17_tests" -hint = """ -You can call a function right where you're passing arguments to `assert!`. So -you could do something like `assert!(having_fun())`. - -If you want to check that you indeed get `false`, you can negate the result of -what you're doing using `!`, like `assert!(!having_fun())`.""" - -[[exercises]] -name = "tests4" -dir = "17_tests" -hint = """ -We expect method `Rectangle::new()` to panic for negative values. - -To handle that you need to add a special attribute to the test function. - -You can refer to the docs: -https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic""" - -# STANDARD LIBRARY TYPES - -[[exercises]] -name = "iterators1" -dir = "18_iterators" -hint = """ -Step 1: - -We need to apply something to the collection `my_fav_fruits` before we start to -go through it. What could that be? Take a look at the struct definition for a -vector for inspiration: -https://doc.rust-lang.org/std/vec/struct.Vec.html - -Step 2 & step 3: - -Very similar to the lines above and below. You've got this! - -Step 4: - -An iterator goes through all elements in a collection, but what if we've run -out of elements? What should we expect here? If you're stuck, take a look at -https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. -""" - -[[exercises]] -name = "iterators2" -dir = "18_iterators" -hint = """ -Step 1: - -The variable `first` is a `char`. It needs to be capitalized and added to the -remaining characters in `c` in order to return the correct `String`. - -The remaining characters in `c` can be viewed as a string slice using the -`as_str` method. - -The documentation for `char` contains many useful methods. -https://doc.rust-lang.org/std/primitive.char.html - -Step 2: - -Create an iterator from the slice. Transform the iterated values by applying -the `capitalize_first` function. Remember to `collect` the iterator. - -Step 3: - -This is surprisingly similar to the previous solution. `collect` is very -powerful and very general. Rust just needs to know the desired type.""" - -[[exercises]] -name = "iterators3" -dir = "18_iterators" -hint = """ -The `divide` function needs to return the correct error when even division is -not possible. - -The `division_results` variable needs to be collected into a collection type. - -The `result_with_list` function needs to return a single `Result` where the -success case is a vector of integers and the failure case is a `DivisionError`. - -The `list_of_results` function needs to return a vector of results. - -See https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect for -how the `FromIterator` trait is used in `collect()`. This trait is REALLY -powerful! It can make the solution to this exercise infinitely easier.""" - -[[exercises]] -name = "iterators4" -dir = "18_iterators" -hint = """ -In an imperative language, you might write a `for` loop that updates a mutable -variable. Or, you might write code utilizing recursion and a match clause. In -Rust you can take another functional approach, computing the factorial -elegantly with ranges and iterators. - -Hint 2: Check out the `fold` and `rfold` methods!""" - -[[exercises]] -name = "iterators5" -dir = "18_iterators" -hint = """ -The documentation for the `std::iter::Iterator` trait contains numerous methods -that would be helpful here. - -The `collection` variable in `count_collection_iterator` is a slice of -`HashMap`s. It needs to be converted into an iterator in order to use the -iterator methods. - -The `fold` method can be useful in the `count_collection_iterator` function. - -For a further challenge, consult the documentation for `Iterator` to find -a different method that could make your code more compact than using `fold`.""" - -# SMART POINTERS - -[[exercises]] -name = "box1" -dir = "19_smart_pointers" -hint = """ -Step 1: - -The compiler's message should help: since we cannot store the value of the -actual type when working with recursive types, we need to store a reference -(pointer) to its value. - -We should, therefore, place our `List` inside a `Box`. More details in the book -here: https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes - -Step 2: - -Creating an empty list should be fairly straightforward (hint: peek at the -assertions). - -For a non-empty list keep in mind that we want to use our `Cons` "list builder". -Although the current list is one of integers (`i32`), feel free to change the -definition and try other types! -""" - -[[exercises]] -name = "rc1" -dir = "19_smart_pointers" -hint = """ -This is a straightforward exercise to use the `Rc` type. Each `Planet` has -ownership of the `Sun`, and uses `Rc::clone()` to increment the reference count -of the `Sun`. - -After using `drop()` to move the `Planet`s out of scope individually, the -reference count goes down. - -In the end the `Sun` only has one reference again, to itself. - -See more at: https://doc.rust-lang.org/book/ch15-04-rc.html - -* Unfortunately Pluto is no longer considered a planet :( -""" - -[[exercises]] -name = "arc1" -dir = "19_smart_pointers" -test = false -hint = """ -Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order -to avoid creating a copy of `numbers`, you'll need to create `child_numbers` -inside the loop but still in the main thread. - -`child_numbers` should be a clone of the `Arc` of the numbers instead of a -thread-local copy of the numbers. - -This is a simple exercise if you understand the underlying concepts, but if this -is too much of a struggle, consider reading through all of Chapter 16 in the -book: -https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html -""" - -[[exercises]] -name = "cow1" -dir = "19_smart_pointers" -hint = """ -If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is -called. - -Check out https://doc.rust-lang.org/std/borrow/enum.Cow.html for documentation -on the `Cow` type. -""" - -# THREADS - -[[exercises]] -name = "threads1" -dir = "20_threads" -test = false -hint = """ -`JoinHandle` is a struct that is returned from a spawned thread: -https://doc.rust-lang.org/std/thread/fn.spawn.html - -A challenge with multi-threaded applications is that the main thread can -finish before the spawned threads are completed. -https://doc.rust-lang.org/book/ch16-01-threads.html#waiting-for-all-threads-to-finish-using-join-handles - -Use the `JoinHandle`s to wait for each thread to finish and collect their -results. - -https://doc.rust-lang.org/std/thread/struct.JoinHandle.html -""" - -[[exercises]] -name = "threads2" -dir = "20_threads" -test = false -hint = """ -`Arc` is an Atomic Reference Counted pointer that allows safe, shared access -to **immutable** data. But we want to *change* the number of `jobs_completed` -so we'll need to also use another type that will only allow one thread to -mutate the data at a time. Take a look at this section of the book: -https://doc.rust-lang.org/book/ch16-03-shared-state.html#atomic-reference-counting-with-arct - -Keep reading if you'd like more hints :) - -Do you now have an `Arc>` at the beginning of `main`? Like: -``` -let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 })); -``` - -Similar to the code in the following example in the book: -https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads -""" - -[[exercises]] -name = "threads3" -dir = "20_threads" -hint = """ -An alternate way to handle concurrency between threads is to use an `mpsc` -(multiple producer, single consumer) channel to communicate. - -With both a sending end and a receiving end, it's possible to send values in -one thread and receive them in another. - -Multiple producers are possible by using clone() to create a duplicate of the -original sending end. - -See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. -""" - -# MACROS - -[[exercises]] -name = "macros1" -dir = "21_macros" -test = false -hint = """ -When you call a macro, you need to add something special compared to a -regular function call. If you're stuck, take a look at what's inside -`my_macro`.""" - -[[exercises]] -name = "macros2" -dir = "21_macros" -test = false -hint = """ -Macros don't quite play by the same rules as the rest of Rust, in terms of -what's available where. - -Unlike other things in Rust, the order of "where you define a macro" versus -"where you use it" actually matters.""" - -[[exercises]] -name = "macros3" -dir = "21_macros" -test = false -hint = """ -In order to use a macro outside of its module, you need to do something -special to the module to lift the macro out into its parent. - -The same trick also works on "extern crate" statements for crates that have -exported macros, if you've seen any of those around.""" - -[[exercises]] -name = "macros4" -dir = "21_macros" -test = false -hint = """ -You only need to add a single character to make this compile. - -The way macros are written, it wants to see something between each "macro arm", -so it can separate them. - -That's all the macro exercises we have in here, but it's barely even scratching -the surface of what you can do with Rust's macros. For a more thorough -introduction, you can have a read through 'The Little Book of Rust Macros': -https://veykril.github.io/tlborm/""" - -# CLIPPY - -[[exercises]] -name = "clippy1" -dir = "22_clippy" -test = false -strict_clippy = true -hint = """ -Rust stores the highest precision version of any long or infinite precision -mathematical constants in the Rust standard library: -https://doc.rust-lang.org/stable/std/f32/consts/index.html - -We may be tempted to use our own approximations for certain mathematical -constants, but clippy recognizes those imprecise mathematical constants as a -source of potential error. - -See the suggestions of the clippy warning in compile output and use the -appropriate replacement constant from `std::f32::consts`...""" - -[[exercises]] -name = "clippy2" -dir = "22_clippy" -test = false -strict_clippy = true -hint = """ -`for` loops over `Option` values are more clearly expressed as an `if let`""" - -[[exercises]] -name = "clippy3" -dir = "22_clippy" -test = false -strict_clippy = true -hint = "No hints this time!" - -# TYPE CONVERSIONS - -[[exercises]] -name = "using_as" -dir = "23_conversions" -hint = """ -Use the `as` operator to cast one of the operands in the last line of the -`average` function into the expected return type.""" - -[[exercises]] -name = "from_into" -dir = "23_conversions" -hint = """ -Follow the steps provided right before the `From` implementation""" - -[[exercises]] -name = "from_str" -dir = "23_conversions" -hint = """ -The implementation of `FromStr` should return an `Ok` with a `Person` object, -or an `Err` with an error if the string is not valid. - -This is almost like the `from_into` exercise, but returning errors instead -of falling back to a default value. - -Look at the test cases to see which error variants to return. - -Another hint: You can use the `map_err` method of `Result` with a function -or a closure to wrap the error from `parse::`. - -Yet another hint: If you would like to propagate errors by using the `?` -operator in your solution, you might want to look at -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html -""" - -[[exercises]] -name = "try_from_into" -dir = "23_conversions" -hint = """ -Follow the steps provided right before the `TryFrom` implementation. -You can also use the example at -https://doc.rust-lang.org/std/convert/trait.TryFrom.html - -Is there an implementation of `TryFrom` in the standard library that -can both do the required integer conversion and check the range of the input? - -Another hint: Look at the test cases to see which error variants to return. - -Yet another hint: You can use the `map_err` or `or` methods of `Result` to -convert errors. - -Yet another hint: If you would like to propagate errors by using the `?` -operator in your solution, you might want to look at -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html - -Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" - -[[exercises]] -name = "as_ref_mut" -dir = "23_conversions" -hint = """ -Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml deleted file mode 120000 index 37952912..00000000 --- a/rustlings-macros/info.toml +++ /dev/null @@ -1 +0,0 @@ -../info.toml \ No newline at end of file diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml new file mode 100644 index 00000000..4204f274 --- /dev/null +++ b/rustlings-macros/info.toml @@ -0,0 +1,1286 @@ +format_version = 1 + +welcome_message = """Is this your first time? Don't worry, Rustlings is made for beginners! +We are going to teach you a lot of things about Rust, but before we can +get started, here are some notes about how Rustlings operates: + +1. The central concept behind Rustlings is that you solve exercises. These + exercises usually contain some compiler or logic errors which cause the + exercise to fail compilation or testing. It's your job to find all errors + and fix them! +2. Make sure to have your editor open in the `rustlings/` directory. Rustlings + will show you the path of the current exercise under the progress bar. Open + the exercise file in your editor, fix errors and save the file. Rustlings will + automatically detect the file change and rerun the exercise. If all errors are + fixed, Rustlings will ask you to move on to the next exercise. +3. If you're stuck on an exercise, enter `h` (or `hint`) to show a hint. +4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! + (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, + other learners do too so you can help each other out! +""" + +final_message = """We hope you enjoyed learning about the various aspects of Rust! +If you noticed any issues, don't hesitate to report them on Github. +You can also contribute your own exercises to help the greater community! + +Before reporting an issue or contributing, please read our guidelines: +https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md +""" + +# INTRO + +# TODO: Update exercise +[[exercises]] +name = "intro1" +dir = "00_intro" +test = false +# TODO: Fix hint +hint = """Enter `n` (or `next`) followed by ENTER to move on to the next exercise""" + +[[exercises]] +name = "intro2" +dir = "00_intro" +test = false +hint = """ +The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" + +# VARIABLES + +[[exercises]] +name = "variables1" +dir = "01_variables" +test = false +hint = """ +The declaration in the first line in the main function is missing a keyword +that is needed in Rust to create a new variable binding.""" + +[[exercises]] +name = "variables2" +dir = "01_variables" +test = false +hint = """ +The compiler message is saying that Rust cannot infer the type that the +variable binding `x` has with what is given here. + +What happens if you annotate the first line in the main function with a type +annotation? + +What if you give `x` a value? + +What if you do both? + +What type should `x` be, anyway? + +What if `x` is the same type as `10`? What if it's a different type?""" + +[[exercises]] +name = "variables3" +dir = "01_variables" +test = false +hint = """ +Oops! In this exercise, we have a variable binding that we've created on in the +first line in the `main` function, and we're trying to use it in the next line, +but we haven't given it a value. + +We can't print out something that isn't there; try giving `x` a value! + +This is an error that can cause bugs that's very easy to make in any +programming language -- thankfully the Rust compiler has caught this for us!""" + +[[exercises]] +name = "variables4" +dir = "01_variables" +test = false +hint = """ +In Rust, variable bindings are immutable by default. But here we're trying +to reassign a different value to `x`! There's a keyword we can use to make +a variable binding mutable instead.""" + +[[exercises]] +name = "variables5" +dir = "01_variables" +test = false +hint = """ +In `variables4` we already learned how to make an immutable variable mutable +using a special keyword. Unfortunately this doesn't help us much in this +exercise because we want to assign a different typed value to an existing +variable. Sometimes you may also like to reuse existing variable names because +you are just converting values to different types like in this exercise. + +Fortunately Rust has a powerful solution to this problem: 'Shadowing'! +You can read more about 'Shadowing' in the book's section 'Variables and +Mutability': +https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing + +Try to solve this exercise afterwards using this technique.""" + +[[exercises]] +name = "variables6" +dir = "01_variables" +test = false +hint = """ +We know about variables and mutability, but there is another important type of +variable available: constants. + +Constants are always immutable and they are declared with keyword `const` rather +than keyword `let`. + +Constants types must also always be annotated. + +Read more about constants and the differences between variables and constants +under 'Constants' in the book's section 'Variables and Mutability': +https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#constants +""" + +# FUNCTIONS + +[[exercises]] +name = "functions1" +dir = "02_functions" +test = false +hint = """ +This main function is calling a function that it expects to exist, but the +function doesn't exist. It expects this function to have the name `call_me`. +It expects this function to not take any arguments and not return a value. +Sounds a lot like `main`, doesn't it?""" + +[[exercises]] +name = "functions2" +dir = "02_functions" +test = false +hint = """ +Rust requires that all parts of a function's signature have type annotations, +but `call_me` is missing the type annotation of `num`.""" + +[[exercises]] +name = "functions3" +dir = "02_functions" +test = false +hint = """ +This time, the function *declaration* is okay, but there's something wrong +with the place where we're calling the function.""" + +[[exercises]] +name = "functions4" +dir = "02_functions" +test = false +hint = """ +The error message points to the function `sale_price` and says it expects a type +after the `->`. This is where the function's return type should be -- take a +look at the `is_even` function for an example!""" + +[[exercises]] +name = "functions5" +dir = "02_functions" +test = false +hint = """ +This is a really common error that can be fixed by removing one character. +It happens because Rust distinguishes between expressions and statements: +expressions return a value based on their operand(s), and statements simply +return a `()` type which behaves just like `void` in C/C++ language. + +We want to return a value of `i32` type from the `square` function, but it is +returning a `()` type... + +They are not the same. There are two solutions: +1. Add a `return` ahead of `num * num;` +2. remove `;`, make it to be `num * num`""" + +# IF + +[[exercises]] +name = "if1" +dir = "03_if" +hint = """ +It's possible to do this in one line if you would like! + +Some similar examples from other languages: +- In C(++) this would be: `a > b ? a : b` +- In Python this would be: `a if a > b else b` + +Remember in Rust that: +- the `if` condition does not need to be surrounded by parentheses +- `if`/`else` conditionals are expressions +- Each condition is followed by a `{}` block.""" + +[[exercises]] +name = "if2" +dir = "03_if" +hint = """ +For that first compiler error, it's important in Rust that each conditional +block returns the same type! To get the tests passing, you will need a couple +conditions checking different input values.""" + +[[exercises]] +name = "if3" +dir = "03_if" +hint = """ +In Rust, every arm of an `if` expression has to return the same type of value. +Make sure the type is consistent across all arms.""" + +# QUIZ 1 + +[[exercises]] +name = "quiz1" +dir = "quizzes" +hint = "No hints this time ;)" + +# PRIMITIVE TYPES + +[[exercises]] +name = "primitive_types1" +dir = "04_primitive_types" +test = false +hint = "No hints this time ;)" + +[[exercises]] +name = "primitive_types2" +dir = "04_primitive_types" +test = false +hint = "No hints this time ;)" + +[[exercises]] +name = "primitive_types3" +dir = "04_primitive_types" +test = false +hint = """ +There's a shorthand to initialize Arrays with a certain size that does not +require you to type in 100 items (but you certainly can if you want!). + +For example, you can do: +``` +let array = ["Are we there yet?"; 10]; +``` + +Bonus: what are some other things you could have that would return `true` +for `a.len() >= 100`?""" + +[[exercises]] +name = "primitive_types4" +dir = "04_primitive_types" +hint = """ +Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section +of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use the +starting and ending (plus one) indices of the items in the `Array` that you +want to end up in the slice. + +If you're curious why the first argument of `assert_eq!` does not have an +ampersand for a reference since the second argument is a reference, take a look +at the coercion chapter of the nomicon: +https://doc.rust-lang.org/nomicon/coercions.html""" + +[[exercises]] +name = "primitive_types5" +dir = "04_primitive_types" +test = false +hint = """ +Take a look at the 'Data Types -> The Tuple Type' section of the book: +https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type +Particularly the part about destructuring (second to last example in the +section). + +You'll need to make a pattern to bind `name` and `age` to the appropriate parts +of the tuple. You can do it!!""" + +[[exercises]] +name = "primitive_types6" +dir = "04_primitive_types" +hint = """ +While you could use a destructuring `let` for the tuple here, try +indexing into it instead, as explained in the last example of the +'Data Types -> The Tuple Type' section of the book: +https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type +Now you have another tool in your toolbox!""" + +# VECS + +[[exercises]] +name = "vecs1" +dir = "05_vecs" +hint = """ +In Rust, there are two ways to define a Vector. +1. One way is to use the `Vec::new()` function to create a new vector + and fill it with the `push()` method. +2. The second way, which is simpler is to use the `vec![]` macro and + define your elements inside the square brackets. + +Check this chapter: https://doc.rust-lang.org/stable/book/ch08-01-vectors.html +of the Rust book to learn more. +""" + +[[exercises]] +name = "vecs2" +dir = "05_vecs" +hint = """ +In the first function we are looping over the Vector and getting a reference to +one `element` at a time. + +To modify the value of that `element` we need to use the `*` dereference +operator. You can learn more in this chapter of the Rust book: +https://doc.rust-lang.org/stable/book/ch08-01-vectors.html#iterating-over-the-values-in-a-vector + +In the second function this dereferencing is not necessary, because the `map` +function expects the new value to be returned. + +After you've completed both functions, decide for yourself which approach you +like better. + +What do you think is the more commonly used pattern under Rust developers? +""" + +# MOVE SEMANTICS + +[[exercises]] +name = "move_semantics1" +dir = "06_move_semantics" +hint = """ +So you've got the "cannot borrow immutable local variable `vec` as mutable" +error on the line where we push an element to the vector, right? + +The fix for this is going to be adding one keyword, and the addition is NOT on +the line where we push to the vector (where the error is). + +Also: Try accessing `vec0` after having called `fill_vec()`. See what +happens!""" + +[[exercises]] +name = "move_semantics2" +dir = "06_move_semantics" +hint = """ +When running this exercise for the first time, you'll notice an error about +"borrow of moved value". In Rust, when an argument is passed to a function and +it's not explicitly returned, you can't use the original variable anymore. +We call this "moving" a variable. When we pass `vec0` into `fill_vec`, it's +being "moved" into `vec1`, meaning we can't access `vec0` anymore after the +fact. + +Rust provides a couple of different ways to mitigate this issue, feel free to +try them all: +1. You could make another, separate version of the data that's in `vec0` and + pass that to `fill_vec` instead. +2. Make `fill_vec` borrow its argument instead of taking ownership of it, + and then copy the data within the function (`vec.clone()`) in order to + return an owned `Vec`. +""" + +[[exercises]] +name = "move_semantics3" +dir = "06_move_semantics" +hint = """ +The difference between this one and the previous ones is that the first line +of `fn fill_vec` that had `let mut vec = vec;` is no longer there. You can, +instead of adding that line back, add `mut` in one place that will change +an existing binding to be a mutable binding instead of an immutable one :)""" + +[[exercises]] +name = "move_semantics4" +dir = "06_move_semantics" +hint = """ +Stop reading whenever you feel like you have enough direction :) Or try +doing one step and then fixing the compiler errors that result! +So the end goal is to: + - get rid of the first line in main that creates the new vector + - so then `vec0` doesn't exist, so we can't pass it to `fill_vec` + - `fill_vec` has had its signature changed, which our call should reflect + - since we're not creating a new vec in `main` anymore, we need to create + a new vec in `fill_vec`, and fill it with the expected values""" + +[[exercises]] +name = "move_semantics5" +dir = "06_move_semantics" +hint = """ +Carefully reason about the range in which each mutable reference is in +scope. Does it help to update the value of referent (`x`) immediately after +the mutable reference is taken? Read more about 'Mutable References' +in the book's section 'References and Borrowing': +https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references. +""" + +[[exercises]] +name = "move_semantics6" +dir = "06_move_semantics" +test = false +hint = """ +To find the answer, you can consult the book section "References and Borrowing": +https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html + +The first problem is that `get_char` is taking ownership of the string. So +`data` is moved and can't be used for `string_uppercase`. `data` is moved to +`get_char` first, meaning that `string_uppercase` cannot manipulate the data. + +Once you've fixed that, `string_uppercase`'s function signature will also need +to be adjusted. + +Can you figure out how? + +Another hint: it has to do with the `&` character.""" + +# STRUCTS + +[[exercises]] +name = "structs1" +dir = "07_structs" +hint = """ +Rust has more than one type of struct. Three actually, all variants are used to +package related data together. + +There are normal (or classic) structs. These are named collections of related +data stored in fields. + +Tuple structs are basically just named tuples. + +Finally, Unit-like structs. These don't have any fields and are useful for +generics. + +In this exercise you need to complete and implement one of each kind. +Read more about structs in The Book: +https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" + +[[exercises]] +name = "structs2" +dir = "07_structs" +hint = """ +Creating instances of structs is easy, all you need to do is assign some values +to its fields. + +There are however some shortcuts that can be taken when instantiating structs. +Have a look in The Book, to find out more: +https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax""" + +[[exercises]] +name = "structs3" +dir = "07_structs" +hint = """ +For `is_international`: What makes a package international? Seems related to +the places it goes through right? + +For `get_fees`: This method takes an additional argument, is there a field in +the `Package` struct that this relates to? + +Have a look in The Book, to find out more about method implementations: +https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" + +# ENUMS + +[[exercises]] +name = "enums1" +dir = "08_enums" +test = false +hint = "No hints this time ;)" + +[[exercises]] +name = "enums2" +dir = "08_enums" +test = false +hint = """ +You can create enumerations that have different variants with different types +such as no data, anonymous structs, a single string, tuples, ...etc""" + +[[exercises]] +name = "enums3" +dir = "08_enums" +hint = """ +As a first step, you can define enums to compile this code without errors. + +And then create a match expression in `process()`. + +Note that you need to deconstruct some message variants in the match expression +to get value in the variant.""" + +# STRINGS + +[[exercises]] +name = "strings1" +dir = "09_strings" +test = false +hint = """ +The `current_favorite_color` function is currently returning a string slice +with the `'static` lifetime. We know this because the data of the string lives +in our code itself -- it doesn't come from a file or user input or another +program -- so it will live as long as our program lives. + +But it is still a string slice. There's one way to create a `String` by +converting a string slice covered in the Strings chapter of the book, and +another way that uses the `From` trait.""" + +[[exercises]] +name = "strings2" +dir = "09_strings" +test = false +hint = """ +Yes, it would be really easy to fix this by just changing the value bound to +`word` to be a string slice instead of a `String`, wouldn't it?? There is a way +to add one character to the `if` statement, though, that will coerce the +`String` into a string slice. + +Side note: If you're interested in learning about how this kind of reference +conversion works, you can jump ahead in the book and read this part in the +smart pointers chapter: +https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods""" + +[[exercises]] +name = "strings3" +dir = "09_strings" +hint = """ +There's tons of useful standard library functions for strings. Let's try and use some of them: +https://doc.rust-lang.org/std/string/struct.String.html#method.trim + +For the `compose_me` method: You can either use the `format!` macro, or convert +the string slice into an owned string, which you can then freely extend.""" + +[[exercises]] +name = "strings4" +dir = "09_strings" +test = false +hint = "No hints this time ;)" + +# MODULES + +[[exercises]] +name = "modules1" +dir = "10_modules" +test = false +hint = """ +Everything is private in Rust by default-- but there's a keyword we can use +to make something public! The compiler error should point to the thing that +needs to be public.""" + +[[exercises]] +name = "modules2" +dir = "10_modules" +test = false +hint = """ +The delicious_snacks module is trying to present an external interface that is +different than its internal structure (the `fruits` and `veggies` modules and +associated constants). Complete the `use` statements to fit the uses in main and +find the one keyword missing for both constants. + +Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use""" + +[[exercises]] +name = "modules3" +dir = "10_modules" +test = false +hint = """ +`UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a +`use` statement for these two to bring them into scope. You can use nested +paths or the glob operator to bring these two in using only one line.""" + +# HASHMAPS + +[[exercises]] +name = "hashmaps1" +dir = "11_hashmaps" +hint = """ +Hint 1: Take a look at the return type of the function to figure out + the type for the `basket`. + +Hint 2: Number of fruits should be at least 5. And you have to put + at least three different types of fruits. +""" + +[[exercises]] +name = "hashmaps2" +dir = "11_hashmaps" +hint = """ +Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. +Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value +""" + +[[exercises]] +name = "hashmaps3" +dir = "11_hashmaps" +hint = """ +Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert + entries corresponding to each team in the scores table. + +Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value + +Hint 2: If there is already an entry for a given key, the value returned by + `entry()` can be updated based on the existing value. + +Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-value-based-on-the-old-value +""" + +# QUIZ 2 + +[[exercises]] +name = "quiz2" +dir = "quizzes" +hint = "No hints this time ;)" + +# OPTIONS + +[[exercises]] +name = "options1" +dir = "12_options" +hint = """ +Options can have a `Some` value, with an inner value, or a `None` value, +without an inner value. + +There's multiple ways to get at the inner value, you can use `unwrap`, or +pattern match. Unwrapping is the easiest, but how do you do it safely so that +it doesn't panic in your face later?""" + +[[exercises]] +name = "options2" +dir = "12_options" +hint = """ +Check out: + +- https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html +- https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html + +Remember that `Option`s can be stacked in `if let` and `while let`. + +For example: `Some(Some(variable)) = variable2` + +Also see `Option::flatten` +""" + +[[exercises]] +name = "options3" +dir = "12_options" +test = false +hint = """ +The compiler says a partial move happened in the `match` statement. How can +this be avoided? The compiler shows the correction needed. + +After making the correction as suggested by the compiler, do read: +https://doc.rust-lang.org/std/keyword.ref.html""" + +# ERROR HANDLING + +[[exercises]] +name = "errors1" +dir = "13_error_handling" +hint = """ +`Ok` and `Err` are the two variants of `Result`, so what the tests are saying +is that `generate_nametag_text` should return a `Result` instead of an `Option`. + +To make this change, you'll need to: + - update the return type in the function signature to be a `Result` that could be the variants `Ok(String)` and `Err(String)` + - change the body of the function to return `Ok(stuff)` where it currently + returns `Some(stuff)` + - change the body of the function to return `Err(error message)` where it + currently returns `None`""" + +[[exercises]] +name = "errors2" +dir = "13_error_handling" +hint = """ +One way to handle this is using a `match` statement on +`item_quantity.parse::()` where the cases are `Ok(something)` and +`Err(something)`. + +This pattern is very common in Rust, though, so there's a `?` operator that +does pretty much what you would make that match statement do for you! + +Take a look at this section of the 'Error Handling' chapter: +https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator +and give it a try!""" + +[[exercises]] +name = "errors3" +dir = "13_error_handling" +test = false +hint = """ +If other functions can return a `Result`, why shouldn't `main`? It's a fairly +common convention to return something like `Result<(), ErrorType>` from your +main function. + +The unit (`()`) type is there because nothing is really needed in terms of +positive results.""" + +[[exercises]] +name = "errors4" +dir = "13_error_handling" +hint = """ +`PositiveNonzeroInteger::new` is always creating a new instance and returning +an `Ok` result. + +It should be doing some checking, returning an `Err` result if those checks +fail, and only returning an `Ok` result if those checks determine that +everything is... okay :)""" + +[[exercises]] +name = "errors5" +dir = "13_error_handling" +test = false +hint = """ +There are two different possible `Result` types produced within `main()`, which +are propagated using `?` operators. How do we declare a return type from +`main()` that allows both? + +Under the hood, the `?` operator calls `From::from` on the error value to +convert it to a boxed trait object, a `Box`. This boxed trait +object is polymorphic, and since all errors implement the `error::Error` trait, +we can capture lots of different errors in one "Box" object. + +Check out this section of the book: +https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator + +Read more about boxing errors: +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/boxing_errors.html + +Read more about using the `?` operator with boxed errors: +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html +""" + +[[exercises]] +name = "errors6" +dir = "13_error_handling" +hint = """ +This exercise uses a completed version of `PositiveNonzeroInteger` from +errors4. + +Below the line that `TODO` asks you to change, there is an example of using +the `map_err()` method on a `Result` to transform one type of error into +another. Try using something similar on the `Result` from `parse()`. You +might use the `?` operator to return early from the function, or you might +use a `match` expression, or maybe there's another way! + +You can create another function inside `impl ParsePosNonzeroError` to use +with `map_err()`. + +Read more about `map_err()` in the `std::result` documentation: +https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" + +# Generics + +[[exercises]] +name = "generics1" +dir = "14_generics" +test = false +hint = """ +Vectors in Rust make use of generics to create dynamically sized arrays of any +type. + +You need to tell the compiler what type we are pushing onto this vector.""" + +[[exercises]] +name = "generics2" +dir = "14_generics" +hint = """ +Currently we are wrapping only values of type `u32`. + +Maybe we could update the explicit references to this data type somehow? + +If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions +""" + +# TRAITS + +[[exercises]] +name = "traits1" +dir = "15_traits" +hint = """ +A discussion about Traits in Rust can be found at: +https://doc.rust-lang.org/book/ch10-02-traits.html +""" + +[[exercises]] +name = "traits2" +dir = "15_traits" +hint = """ +Notice how the trait takes ownership of `self`, and returns `Self`. + +Try mutating the incoming string vector. Have a look at the tests to see +what the result should look like! + +Vectors provide suitable methods for adding an element at the end. See +the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" + +[[exercises]] +name = "traits3" +dir = "15_traits" +hint = """ +Traits can have a default implementation for functions. Structs that implement +the trait can then use the default version of these functions if they choose not +to implement the function themselves. + +See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations +""" + +[[exercises]] +name = "traits4" +dir = "15_traits" +hint = """ +Instead of using concrete types as parameters you can use traits. Try replacing +the '??' with 'impl ' + +See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters +""" + +[[exercises]] +name = "traits5" +dir = "15_traits" +test = false +hint = """ +To ensure a parameter implements multiple traits use the '+ syntax'. Try +replacing the '??' with 'impl <> + <>'. + +See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax +""" + +# QUIZ 3 + +[[exercises]] +name = "quiz3" +dir = "quizzes" +hint = """ +To find the best solution to this challenge you're going to need to think back +to your knowledge of traits, specifically 'Trait Bound Syntax' + +You may also need this: `use std::fmt::Display;`.""" + +# LIFETIMES + +[[exercises]] +name = "lifetimes1" +dir = "16_lifetimes" +test = false +hint = """ +Let the compiler guide you. Also take a look at the book if you need help: +https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" + +[[exercises]] +name = "lifetimes2" +dir = "16_lifetimes" +test = false +hint = """ +Remember that the generic lifetime `'a` will get the concrete lifetime that is +equal to the smaller of the lifetimes of `x` and `y`. + +You can take at least two paths to achieve the desired result while keeping the +inner block: +1. Move the `string2` declaration to make it live as long as `string1` (how is + `result` declared?) +2. Move `println!` into the inner block""" + +[[exercises]] +name = "lifetimes3" +dir = "16_lifetimes" +test = false +hint = """ +If you use a lifetime annotation in a struct's fields, where else does it need +to be added?""" + +# TESTS + +[[exercises]] +name = "tests1" +dir = "17_tests" +hint = """ +You don't even need to write any code to test -- you can just test values and +run that, even though you wouldn't do that in real life. :) + +`assert!` is a macro that needs an argument. Depending on the value of the +argument, `assert!` will do nothing (in which case the test will pass) or +`assert!` will panic (in which case the test will fail). + +So try giving different values to `assert!` and see which ones compile, which +ones pass, and which ones fail :)""" + +[[exercises]] +name = "tests2" +dir = "17_tests" +hint = """ +Like the previous exercise, you don't need to write any code to get this test +to compile and run. + +`assert_eq!` is a macro that takes two arguments and compares them. Try giving +it two values that are equal! Try giving it two arguments that are different! +Try giving it two values that are of different types! Try switching which +argument comes first and which comes second!""" + +[[exercises]] +name = "tests3" +dir = "17_tests" +hint = """ +You can call a function right where you're passing arguments to `assert!`. So +you could do something like `assert!(having_fun())`. + +If you want to check that you indeed get `false`, you can negate the result of +what you're doing using `!`, like `assert!(!having_fun())`.""" + +[[exercises]] +name = "tests4" +dir = "17_tests" +hint = """ +We expect method `Rectangle::new()` to panic for negative values. + +To handle that you need to add a special attribute to the test function. + +You can refer to the docs: +https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic""" + +# STANDARD LIBRARY TYPES + +[[exercises]] +name = "iterators1" +dir = "18_iterators" +hint = """ +Step 1: + +We need to apply something to the collection `my_fav_fruits` before we start to +go through it. What could that be? Take a look at the struct definition for a +vector for inspiration: +https://doc.rust-lang.org/std/vec/struct.Vec.html + +Step 2 & step 3: + +Very similar to the lines above and below. You've got this! + +Step 4: + +An iterator goes through all elements in a collection, but what if we've run +out of elements? What should we expect here? If you're stuck, take a look at +https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. +""" + +[[exercises]] +name = "iterators2" +dir = "18_iterators" +hint = """ +Step 1: + +The variable `first` is a `char`. It needs to be capitalized and added to the +remaining characters in `c` in order to return the correct `String`. + +The remaining characters in `c` can be viewed as a string slice using the +`as_str` method. + +The documentation for `char` contains many useful methods. +https://doc.rust-lang.org/std/primitive.char.html + +Step 2: + +Create an iterator from the slice. Transform the iterated values by applying +the `capitalize_first` function. Remember to `collect` the iterator. + +Step 3: + +This is surprisingly similar to the previous solution. `collect` is very +powerful and very general. Rust just needs to know the desired type.""" + +[[exercises]] +name = "iterators3" +dir = "18_iterators" +hint = """ +The `divide` function needs to return the correct error when even division is +not possible. + +The `division_results` variable needs to be collected into a collection type. + +The `result_with_list` function needs to return a single `Result` where the +success case is a vector of integers and the failure case is a `DivisionError`. + +The `list_of_results` function needs to return a vector of results. + +See https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect for +how the `FromIterator` trait is used in `collect()`. This trait is REALLY +powerful! It can make the solution to this exercise infinitely easier.""" + +[[exercises]] +name = "iterators4" +dir = "18_iterators" +hint = """ +In an imperative language, you might write a `for` loop that updates a mutable +variable. Or, you might write code utilizing recursion and a match clause. In +Rust you can take another functional approach, computing the factorial +elegantly with ranges and iterators. + +Hint 2: Check out the `fold` and `rfold` methods!""" + +[[exercises]] +name = "iterators5" +dir = "18_iterators" +hint = """ +The documentation for the `std::iter::Iterator` trait contains numerous methods +that would be helpful here. + +The `collection` variable in `count_collection_iterator` is a slice of +`HashMap`s. It needs to be converted into an iterator in order to use the +iterator methods. + +The `fold` method can be useful in the `count_collection_iterator` function. + +For a further challenge, consult the documentation for `Iterator` to find +a different method that could make your code more compact than using `fold`.""" + +# SMART POINTERS + +[[exercises]] +name = "box1" +dir = "19_smart_pointers" +hint = """ +Step 1: + +The compiler's message should help: since we cannot store the value of the +actual type when working with recursive types, we need to store a reference +(pointer) to its value. + +We should, therefore, place our `List` inside a `Box`. More details in the book +here: https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes + +Step 2: + +Creating an empty list should be fairly straightforward (hint: peek at the +assertions). + +For a non-empty list keep in mind that we want to use our `Cons` "list builder". +Although the current list is one of integers (`i32`), feel free to change the +definition and try other types! +""" + +[[exercises]] +name = "rc1" +dir = "19_smart_pointers" +hint = """ +This is a straightforward exercise to use the `Rc` type. Each `Planet` has +ownership of the `Sun`, and uses `Rc::clone()` to increment the reference count +of the `Sun`. + +After using `drop()` to move the `Planet`s out of scope individually, the +reference count goes down. + +In the end the `Sun` only has one reference again, to itself. + +See more at: https://doc.rust-lang.org/book/ch15-04-rc.html + +* Unfortunately Pluto is no longer considered a planet :( +""" + +[[exercises]] +name = "arc1" +dir = "19_smart_pointers" +test = false +hint = """ +Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order +to avoid creating a copy of `numbers`, you'll need to create `child_numbers` +inside the loop but still in the main thread. + +`child_numbers` should be a clone of the `Arc` of the numbers instead of a +thread-local copy of the numbers. + +This is a simple exercise if you understand the underlying concepts, but if this +is too much of a struggle, consider reading through all of Chapter 16 in the +book: +https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html +""" + +[[exercises]] +name = "cow1" +dir = "19_smart_pointers" +hint = """ +If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is +called. + +Check out https://doc.rust-lang.org/std/borrow/enum.Cow.html for documentation +on the `Cow` type. +""" + +# THREADS + +[[exercises]] +name = "threads1" +dir = "20_threads" +test = false +hint = """ +`JoinHandle` is a struct that is returned from a spawned thread: +https://doc.rust-lang.org/std/thread/fn.spawn.html + +A challenge with multi-threaded applications is that the main thread can +finish before the spawned threads are completed. +https://doc.rust-lang.org/book/ch16-01-threads.html#waiting-for-all-threads-to-finish-using-join-handles + +Use the `JoinHandle`s to wait for each thread to finish and collect their +results. + +https://doc.rust-lang.org/std/thread/struct.JoinHandle.html +""" + +[[exercises]] +name = "threads2" +dir = "20_threads" +test = false +hint = """ +`Arc` is an Atomic Reference Counted pointer that allows safe, shared access +to **immutable** data. But we want to *change* the number of `jobs_completed` +so we'll need to also use another type that will only allow one thread to +mutate the data at a time. Take a look at this section of the book: +https://doc.rust-lang.org/book/ch16-03-shared-state.html#atomic-reference-counting-with-arct + +Keep reading if you'd like more hints :) + +Do you now have an `Arc>` at the beginning of `main`? Like: +``` +let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 })); +``` + +Similar to the code in the following example in the book: +https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads +""" + +[[exercises]] +name = "threads3" +dir = "20_threads" +hint = """ +An alternate way to handle concurrency between threads is to use an `mpsc` +(multiple producer, single consumer) channel to communicate. + +With both a sending end and a receiving end, it's possible to send values in +one thread and receive them in another. + +Multiple producers are possible by using clone() to create a duplicate of the +original sending end. + +See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. +""" + +# MACROS + +[[exercises]] +name = "macros1" +dir = "21_macros" +test = false +hint = """ +When you call a macro, you need to add something special compared to a +regular function call. If you're stuck, take a look at what's inside +`my_macro`.""" + +[[exercises]] +name = "macros2" +dir = "21_macros" +test = false +hint = """ +Macros don't quite play by the same rules as the rest of Rust, in terms of +what's available where. + +Unlike other things in Rust, the order of "where you define a macro" versus +"where you use it" actually matters.""" + +[[exercises]] +name = "macros3" +dir = "21_macros" +test = false +hint = """ +In order to use a macro outside of its module, you need to do something +special to the module to lift the macro out into its parent. + +The same trick also works on "extern crate" statements for crates that have +exported macros, if you've seen any of those around.""" + +[[exercises]] +name = "macros4" +dir = "21_macros" +test = false +hint = """ +You only need to add a single character to make this compile. + +The way macros are written, it wants to see something between each "macro arm", +so it can separate them. + +That's all the macro exercises we have in here, but it's barely even scratching +the surface of what you can do with Rust's macros. For a more thorough +introduction, you can have a read through 'The Little Book of Rust Macros': +https://veykril.github.io/tlborm/""" + +# CLIPPY + +[[exercises]] +name = "clippy1" +dir = "22_clippy" +test = false +strict_clippy = true +hint = """ +Rust stores the highest precision version of any long or infinite precision +mathematical constants in the Rust standard library: +https://doc.rust-lang.org/stable/std/f32/consts/index.html + +We may be tempted to use our own approximations for certain mathematical +constants, but clippy recognizes those imprecise mathematical constants as a +source of potential error. + +See the suggestions of the clippy warning in compile output and use the +appropriate replacement constant from `std::f32::consts`...""" + +[[exercises]] +name = "clippy2" +dir = "22_clippy" +test = false +strict_clippy = true +hint = """ +`for` loops over `Option` values are more clearly expressed as an `if let`""" + +[[exercises]] +name = "clippy3" +dir = "22_clippy" +test = false +strict_clippy = true +hint = "No hints this time!" + +# TYPE CONVERSIONS + +[[exercises]] +name = "using_as" +dir = "23_conversions" +hint = """ +Use the `as` operator to cast one of the operands in the last line of the +`average` function into the expected return type.""" + +[[exercises]] +name = "from_into" +dir = "23_conversions" +hint = """ +Follow the steps provided right before the `From` implementation""" + +[[exercises]] +name = "from_str" +dir = "23_conversions" +hint = """ +The implementation of `FromStr` should return an `Ok` with a `Person` object, +or an `Err` with an error if the string is not valid. + +This is almost like the `from_into` exercise, but returning errors instead +of falling back to a default value. + +Look at the test cases to see which error variants to return. + +Another hint: You can use the `map_err` method of `Result` with a function +or a closure to wrap the error from `parse::`. + +Yet another hint: If you would like to propagate errors by using the `?` +operator in your solution, you might want to look at +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html +""" + +[[exercises]] +name = "try_from_into" +dir = "23_conversions" +hint = """ +Follow the steps provided right before the `TryFrom` implementation. +You can also use the example at +https://doc.rust-lang.org/std/convert/trait.TryFrom.html + +Is there an implementation of `TryFrom` in the standard library that +can both do the required integer conversion and check the range of the input? + +Another hint: Look at the test cases to see which error variants to return. + +Yet another hint: You can use the `map_err` or `or` methods of `Result` to +convert errors. + +Yet another hint: If you would like to propagate errors by using the `?` +operator in your solution, you might want to look at +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html + +Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" + +[[exercises]] +name = "as_ref_mut" +dir = "23_conversions" +hint = """ +Add `AsRef` or `AsMut` as a trait bound to the functions.""" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index 4417a4f9..6c6067bc 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -15,7 +15,8 @@ struct InfoFile { #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { - let exercises = toml_edit::de::from_str::(include_str!("../info.toml")) + let info_file = include_str!("../info.toml"); + let exercises = toml_edit::de::from_str::(info_file) .expect("Failed to parse `info.toml`") .exercises; @@ -46,6 +47,7 @@ pub fn include_files(_: TokenStream) -> TokenStream { quote! { EmbeddedFiles { + info_file: #info_file, exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*], exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*] } diff --git a/src/embedded.rs b/src/embedded.rs index 23c8d6e4..45f8eca8 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -70,6 +70,7 @@ impl ExerciseDir { } pub struct EmbeddedFiles { + pub info_file: &'static str, exercise_files: &'static [ExerciseFiles], exercise_dirs: &'static [ExerciseDir], } @@ -148,7 +149,7 @@ mod tests { #[test] fn dirs() { - let exercises = toml_edit::de::from_str::(include_str!("../info.toml")) + let exercises = toml_edit::de::from_str::(EMBEDDED_FILES.info_file) .expect("Failed to parse `info.toml`") .exercises; diff --git a/src/info_file.rs b/src/info_file.rs index dbe4f089..14b886b2 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,6 +2,8 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; use std::{fs, io::ErrorKind}; +use crate::embedded::EMBEDDED_FILES; + // Deserialized from the `info.toml` file. #[derive(Deserialize)] pub struct ExerciseInfo { @@ -47,7 +49,7 @@ impl InfoFile { .context("Failed to parse the `info.toml` file")?, Err(e) => { if e.kind() == ErrorKind::NotFound { - return toml_edit::de::from_str(include_str!("../info.toml")) + return toml_edit::de::from_str(EMBEDDED_FILES.info_file) .context("Failed to parse the embedded `info.toml` file"); } From 052573904604896398a6cc7281398fa9fdf8f083 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:20:04 +0200 Subject: [PATCH 287/433] Fix invisible input on Windows --- src/watch.rs | 4 ++-- src/watch/state.rs | 13 +++++------ src/watch/terminal_event.rs | 44 ++++++++++++++----------------------- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/watch.rs b/src/watch.rs index 453d9a43..944d77b4 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -92,8 +92,8 @@ pub fn watch( break; } WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, - WatchEvent::Input(InputEvent::Unrecognized(cmd)) => { - watch_state.handle_invalid_cmd(&cmd)?; + WatchEvent::Input(InputEvent::Unrecognized(input)) => { + watch_state.handle_invalid_input(input)?; } WatchEvent::FileChange { exercise_ind } => { watch_state.run_exercise_with_ind(exercise_ind)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 2cf7521d..f3ffac85 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -166,14 +166,11 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex self.render() } - pub fn handle_invalid_cmd(&mut self, cmd: &str) -> io::Result<()> { - self.writer.write_all(b"Invalid command: ")?; - self.writer.write_all(cmd.as_bytes())?; - if cmd.len() > 1 { - self.writer - .write_all(b" (confusing input can occur after resizing the terminal)")?; - } - self.writer.write_all(b"\n")?; + pub fn handle_invalid_input(&mut self, input: char) -> io::Result<()> { + writeln!( + self.writer, + "Invalid input: {input} (confusing input can occur after resizing the terminal)", + )?; self.show_prompt() } } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 6d790b7c..846bec17 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -9,12 +9,10 @@ pub enum InputEvent { Hint, List, Quit, - Unrecognized(String), + Unrecognized(char), } pub fn terminal_event_handler(tx: Sender, manual_run: bool) { - let mut input = String::with_capacity(8); - let last_input_event = loop { let terminal_event = match event::read() { Ok(v) => v, @@ -28,36 +26,28 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { match terminal_event { Event::Key(key) => { + match key.kind { + KeyEventKind::Release | KeyEventKind::Repeat => continue, + KeyEventKind::Press => (), + } + if key.modifiers != KeyModifiers::NONE { continue; } - match key.kind { - KeyEventKind::Release => continue, - KeyEventKind::Press | KeyEventKind::Repeat => (), - } + if let KeyCode::Char(c) = key.code { + let input_event = match c { + 'n' => InputEvent::Next, + 'h' => InputEvent::Hint, + 'l' => break InputEvent::List, + 'q' => break InputEvent::Quit, + 'r' if manual_run => InputEvent::Run, + _ => InputEvent::Unrecognized(c), + }; - match key.code { - KeyCode::Enter => { - let input_event = match input.trim() { - "n" | "next" => InputEvent::Next, - "h" | "hint" => InputEvent::Hint, - "l" | "list" => break InputEvent::List, - "q" | "quit" => break InputEvent::Quit, - "r" | "run" if manual_run => InputEvent::Run, - _ => InputEvent::Unrecognized(input.clone()), - }; - - if tx.send(WatchEvent::Input(input_event)).is_err() { - return; - } - - input.clear(); + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; } - KeyCode::Char(c) => { - input.push(c); - } - _ => (), } } Event::Resize(_, _) => { From f9e35a4344cd7d51923f1983cf824fb36be92d50 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:32:25 +0200 Subject: [PATCH 288/433] Improve input handling --- src/app_state.rs | 10 +++++++--- src/run.rs | 6 +++++- src/watch.rs | 7 +++---- src/watch/state.rs | 13 +------------ src/watch/terminal_event.rs | 4 ++-- 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 492be345..85639e51 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -21,8 +21,12 @@ const BAD_INDEX_ERR: &str = "The current exercise index is higher than the numbe #[must_use] pub enum ExercisesProgress { + // All exercises are done. AllDone, - Pending, + // The current exercise failed and is still pending. + CurrentPending, + // A new exercise is now pending. + NewPending, } pub enum StateFileStatus { @@ -343,7 +347,7 @@ impl AppState { if let Some(ind) = self.next_pending_exercise_ind() { self.set_current_exercise_ind(ind)?; - return Ok(ExercisesProgress::Pending); + return Ok(ExercisesProgress::NewPending); } writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; @@ -366,7 +370,7 @@ impl AppState { self.write()?; - return Ok(ExercisesProgress::Pending); + return Ok(ExercisesProgress::NewPending); } writeln!(writer, "{}", "ok".green())?; diff --git a/src/run.rs b/src/run.rs index 9b5ddd34..ac974143 100644 --- a/src/run.rs +++ b/src/run.rs @@ -41,7 +41,11 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::Pending => println!( + ExercisesProgress::CurrentPending => println!( + "Current exercise: {}", + app_state.current_exercise().terminal_link(), + ), + ExercisesProgress::NewPending => println!( "Next exercise: {}", app_state.current_exercise().terminal_link(), ), diff --git a/src/watch.rs b/src/watch.rs index 944d77b4..7d4f54bc 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -79,7 +79,8 @@ pub fn watch( match event { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise()? { ExercisesProgress::AllDone => break, - ExercisesProgress::Pending => watch_state.run_current_exercise()?, + ExercisesProgress::CurrentPending => watch_state.render()?, + ExercisesProgress::NewPending => watch_state.run_current_exercise()?, }, WatchEvent::Input(InputEvent::Hint) => { watch_state.show_hint()?; @@ -92,9 +93,7 @@ pub fn watch( break; } WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, - WatchEvent::Input(InputEvent::Unrecognized(input)) => { - watch_state.handle_invalid_input(input)?; - } + WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?, WatchEvent::FileChange { exercise_ind } => { watch_state.run_exercise_with_ind(exercise_ind)?; } diff --git a/src/watch/state.rs b/src/watch/state.rs index f3ffac85..2e985461 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -78,10 +78,7 @@ impl<'a> WatchState<'a> { pub fn next_exercise(&mut self) -> Result { if matches!(self.done_status, DoneStatus::Pending) { - self.writer - .write_all(b"The current exercise isn't done yet\n")?; - self.show_prompt()?; - return Ok(ExercisesProgress::Pending); + return Ok(ExercisesProgress::CurrentPending); } self.app_state.done_current_exercise(&mut self.writer) @@ -165,12 +162,4 @@ When you are done experimenting, enter `n` (or `next`) to move on to the next ex self.show_hint = true; self.render() } - - pub fn handle_invalid_input(&mut self, input: char) -> io::Result<()> { - writeln!( - self.writer, - "Invalid input: {input} (confusing input can occur after resizing the terminal)", - )?; - self.show_prompt() - } } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 846bec17..29a672a0 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -9,7 +9,7 @@ pub enum InputEvent { Hint, List, Quit, - Unrecognized(char), + Unrecognized, } pub fn terminal_event_handler(tx: Sender, manual_run: bool) { @@ -42,7 +42,7 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { 'l' => break InputEvent::List, 'q' => break InputEvent::Quit, 'r' if manual_run => InputEvent::Run, - _ => InputEvent::Unrecognized(c), + _ => InputEvent::Unrecognized, }; if tx.send(WatchEvent::Input(input_event)).is_err() { From d2b5906be226f936481ff3a5cb8fccde5c721524 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:37:32 +0200 Subject: [PATCH 289/433] No more word input --- README.md | 4 ++-- exercises/00_intro/intro1.rs | 2 +- rustlings-macros/info.toml | 4 ++-- src/main.rs | 2 +- src/watch.rs | 2 +- src/watch/state.rs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3ba080f5..0180608e 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ It will rerun the current exercise automatically every time you change the exerc
If detecting file changes in the exercises/ directory fails… (click to expand) -> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` (or `run`) in the watch mode. +> You can add the **`--manual-run`** flag (`rustlings --manual-run`) to manually rerun the current exercise by entering `r` in the watch mode. > > Please [report the issue](https://github.com/rust-lang/rustlings/issues/new) with some information about your operating system and whether you run Rustlings in a container or virtual machine (e.g. WSL). @@ -106,7 +106,7 @@ It will rerun the current exercise automatically every time you change the exerc ### Exercise List -In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` (or `list`) to open the interactive exercise list. +In the [watch mode](#watch-mode) (after launching `rustlings`), you can enter `l` to open the interactive exercise list. The list allows you to… diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index 62bf95f1..bdbf34b0 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,6 +1,6 @@ // We sometimes encourage you to keep trying things on a given exercise, even // after you already figured it out. If you got everything working and feel -// ready for the next exercise, enter `n` (or `next`) in the terminal. +// ready for the next exercise, enter `n` in the terminal. // // The exercise file will be reloaded when you change one of the lines below! // Try adding a new `println!`. diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 4204f274..485665e4 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -13,7 +13,7 @@ get started, here are some notes about how Rustlings operates: the exercise file in your editor, fix errors and save the file. Rustlings will automatically detect the file change and rerun the exercise. If all errors are fixed, Rustlings will ask you to move on to the next exercise. -3. If you're stuck on an exercise, enter `h` (or `hint`) to show a hint. +3. If you're stuck on an exercise, enter `h` to show a hint. 4. If an exercise doesn't make sense to you, feel free to open an issue on GitHub! (https://github.com/rust-lang/rustlings). We look at every issue, and sometimes, other learners do too so you can help each other out! @@ -35,7 +35,7 @@ name = "intro1" dir = "00_intro" test = false # TODO: Fix hint -hint = """Enter `n` (or `next`) followed by ENTER to move on to the next exercise""" +hint = """Enter `n` to move on to the next exercise. You might need to press ENTER after typing `n`.""" [[exercises]] name = "intro2" diff --git a/src/main.rs b/src/main.rs index 15bcc8e3..cf6f0d96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,7 @@ fn press_enter_prompt() -> io::Result<()> { struct Args { #[command(subcommand)] command: Option, - /// Manually run the current exercise using `r` or `run` in the watch mode. + /// Manually run the current exercise using `r` in the watch mode. /// Only use this if Rustlings fails to detect exercise file changes. #[arg(long)] manual_run: bool, diff --git a/src/watch.rs b/src/watch.rs index 7d4f54bc..f72ebf7d 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -123,5 +123,5 @@ The automatic detection of exercise file changes failed :( Please try running `rustlings` again. If you keep getting this error, run `rustlings --manual-run` to deactivate the file watcher. -You need to manually trigger running the current exercise using `r` (or `run`) then. +You need to manually trigger running the current exercise using `r` then. "; diff --git a/src/watch/state.rs b/src/watch/state.rs index 2e985461..c21d7cae 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -127,7 +127,7 @@ impl<'a> WatchState<'a> { self.writer, "{}\n", "Exercise done ✓ -When you are done experimenting, enter `n` (or `next`) to move on to the next exercise 🦀" +When you are done experimenting, enter `n` to move on to the next exercise 🦀" .bold() .green(), )?; From 8b2d9ed50398c4c5c999ab9ab67757770449ed56 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:45:12 +0200 Subject: [PATCH 290/433] Use PartialEq instead of matches! --- src/watch/state.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index c21d7cae..74cf1823 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -13,6 +13,7 @@ use crate::{ terminal_link::TerminalFileLink, }; +#[derive(PartialEq, Eq)] enum DoneStatus { DoneWithSolution(String), DoneWithoutSolution, @@ -77,7 +78,7 @@ impl<'a> WatchState<'a> { } pub fn next_exercise(&mut self) -> Result { - if matches!(self.done_status, DoneStatus::Pending) { + if self.done_status == DoneStatus::Pending { return Ok(ExercisesProgress::CurrentPending); } @@ -91,7 +92,7 @@ impl<'a> WatchState<'a> { write!(self.writer, "{}un/", 'r'.bold())?; } - if !matches!(self.done_status, DoneStatus::Pending) { + if self.done_status != DoneStatus::Pending { write!(self.writer, "{}ext/", 'n'.bold())?; } @@ -122,7 +123,7 @@ impl<'a> WatchState<'a> { )?; } - if !matches!(self.done_status, DoneStatus::Pending) { + if self.done_status != DoneStatus::Pending { writeln!( self.writer, "{}\n", From a4da216a5c52cf5626e1861991d1172c5897e746 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:46:26 +0200 Subject: [PATCH 291/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a6049ed..aebbfc5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,7 +656,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustlings" -version = "6.0.0-beta.6" +version = "6.0.0-beta.7" dependencies = [ "anyhow", "assert_cmd", @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.6" +version = "6.0.0-beta.7" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index f2015cb3..eacae3b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.6" +version = "6.0.0-beta.7" authors = [ "Liv ", "Mo Bitar ", @@ -53,7 +53,7 @@ hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.6" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.7" } serde_json = "1.0.117" serde.workspace = true toml_edit.workspace = true From 7a74a72dc8a7bd906e7733731b427a1cdeee103a Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 02:48:42 +0200 Subject: [PATCH 292/433] Update beta version in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0180608e..c1ce95ce 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.6 +cargo install rustlings@6.0.0-beta.7 ```
@@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.6 - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.6 --locked` +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.7 --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
From f6cf6c611c8b79131e1b6eac3ece7987ba1eaaf5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 04:11:11 +0200 Subject: [PATCH 293/433] Fix Windows terminal links --- README.md | 4 ++-- src/terminal_link.rs | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c1ce95ce..e6ea8de8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.7 +cargo install rustlings@6.0.0-beta.8 ```
@@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.7 - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.7 --locked` +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.8 --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
diff --git a/src/terminal_link.rs b/src/terminal_link.rs index c9e6bced..9bea07d9 100644 --- a/src/terminal_link.rs +++ b/src/terminal_link.rs @@ -7,15 +7,18 @@ pub struct TerminalFileLink<'a>(pub &'a str); impl<'a> Display for TerminalFileLink<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - if let Ok(Some(canonical_path)) = fs::canonicalize(self.0) - .as_deref() - .map(|path| path.to_str()) - { - write!( - f, - "\x1b]8;;file://{}\x1b\\{}\x1b]8;;\x1b\\", - canonical_path, self.0, - ) + let path = fs::canonicalize(self.0); + + if let Some(path) = path.as_deref().ok().and_then(|path| path.to_str()) { + // Windows itself can't handle its verbatim paths. + #[cfg(windows)] + let path = if path.len() > 5 && &path[0..4] == r"\\?\" { + &path[4..] + } else { + path + }; + + write!(f, "\x1b]8;;file://{path}\x1b\\{}\x1b]8;;\x1b\\", self.0) } else { write!(f, "{}", self.0) } From 56eb4a5d650e6bb671fe60b8cc746b19e9c5f3d4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 04:11:29 +0200 Subject: [PATCH 294/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aebbfc5b..7c1d1a6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,7 +656,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustlings" -version = "6.0.0-beta.7" +version = "6.0.0-beta.8" dependencies = [ "anyhow", "assert_cmd", @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.7" +version = "6.0.0-beta.8" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index eacae3b8..e5525901 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.7" +version = "6.0.0-beta.8" authors = [ "Liv ", "Mo Bitar ", @@ -53,7 +53,7 @@ hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.7" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.8" } serde_json = "1.0.117" serde.workspace = true toml_edit.workspace = true From a7bc6d53a56e105b4d8ad558ef533ee7ecb6aaea Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 16:39:38 +0200 Subject: [PATCH 295/433] Only send `Unrecognized` on ENTER if the last input wasn't valid --- src/watch/terminal_event.rs | 45 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 29a672a0..f54af17a 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -13,6 +13,9 @@ pub enum InputEvent { } pub fn terminal_event_handler(tx: Sender, manual_run: bool) { + // Only send `Unrecognized` on ENTER if the last input wasn't valid. + let mut last_input_valid = false; + let last_input_event = loop { let terminal_event = match event::read() { Ok(v) => v, @@ -32,22 +35,42 @@ pub fn terminal_event_handler(tx: Sender, manual_run: bool) { } if key.modifiers != KeyModifiers::NONE { + last_input_valid = false; continue; } - if let KeyCode::Char(c) = key.code { - let input_event = match c { - 'n' => InputEvent::Next, - 'h' => InputEvent::Hint, - 'l' => break InputEvent::List, - 'q' => break InputEvent::Quit, - 'r' if manual_run => InputEvent::Run, - _ => InputEvent::Unrecognized, - }; + let input_event = match key.code { + KeyCode::Enter => { + if last_input_valid { + continue; + } - if tx.send(WatchEvent::Input(input_event)).is_err() { - return; + InputEvent::Unrecognized } + KeyCode::Char(c) => { + let input_event = match c { + 'n' => InputEvent::Next, + 'h' => InputEvent::Hint, + 'l' => break InputEvent::List, + 'q' => break InputEvent::Quit, + 'r' if manual_run => InputEvent::Run, + _ => { + last_input_valid = false; + continue; + } + }; + + last_input_valid = true; + input_event + } + _ => { + last_input_valid = false; + continue; + } + }; + + if tx.send(WatchEvent::Input(input_event)).is_err() { + return; } } Event::Resize(_, _) => { From 17a2d42ffd868e2049c91d7d1adbecd7f9958020 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 16:44:48 +0200 Subject: [PATCH 296/433] Better variable naming --- src/watch/notify_event.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index f66a8342..a2243771 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -9,17 +9,17 @@ pub struct DebounceEventHandler { } impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { - fn handle_event(&mut self, event: DebounceEventResult) { - let event = match event { - Ok(event) => { - let Some(exercise_ind) = event + fn handle_event(&mut self, input_event: DebounceEventResult) { + let output_event = match input_event { + Ok(input_event) => { + let Some(exercise_ind) = input_event .iter() - .filter_map(|event| { - if event.kind != DebouncedEventKind::Any { + .filter_map(|input_event| { + if input_event.kind != DebouncedEventKind::Any { return None; } - let file_name = event.path.file_name()?.to_str()?.as_bytes(); + let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); if file_name.len() < 4 { return None; @@ -46,6 +46,6 @@ impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { // An error occurs when the receiver is dropped. // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.tx.send(event); + let _ = self.tx.send(output_event); } } From 4ae3fcc3caf91d4b22680ed4497c8ee05296eaad Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 17:06:11 +0200 Subject: [PATCH 297/433] Don't skip exercises on file changes --- src/app_state.rs | 4 ++++ src/watch.rs | 2 +- src/watch/state.rs | 9 ++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 85639e51..75014ceb 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -198,6 +198,10 @@ impl AppState { } pub fn set_current_exercise_ind(&mut self, exercise_ind: usize) -> Result<()> { + if exercise_ind == self.current_exercise_ind { + return Ok(()); + } + if exercise_ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); } diff --git a/src/watch.rs b/src/watch.rs index f72ebf7d..2fbc5337 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -95,7 +95,7 @@ pub fn watch( WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise()?, WatchEvent::Input(InputEvent::Unrecognized) => watch_state.render()?, WatchEvent::FileChange { exercise_ind } => { - watch_state.run_exercise_with_ind(exercise_ind)?; + watch_state.handle_file_change(exercise_ind)?; } WatchEvent::TerminalResize => { watch_state.render()?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 74cf1823..60b6d5a8 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -72,7 +72,14 @@ impl<'a> WatchState<'a> { self.render() } - pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<()> { + pub fn handle_file_change(&mut self, exercise_ind: usize) -> Result<()> { + // Don't skip exercises on file changes to avoid confusion from missing exercises. + // Skipping exercises must be explicit in the interactive list. + // But going back to an earlier exercise on file change is fine. + if self.app_state.current_exercise_ind() < exercise_ind { + return Ok(()); + } + self.app_state.set_current_exercise_ind(exercise_ind)?; self.run_current_exercise() } From e80e91faf284a4d20a2a7fd6ecd1c241e5c2e136 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 17:12:58 +0200 Subject: [PATCH 298/433] Thanks Clippy :) --- src/embedded.rs | 8 ++++---- src/exercise.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 45f8eca8..39ade17d 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -96,8 +96,8 @@ impl EmbeddedFiles { } pub fn write_exercise_to_disk(&self, exercise_ind: usize, path: &str) -> Result<()> { - let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind]; - let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind]; + let exercise_files = &self.exercise_files[exercise_ind]; + let dir = &self.exercise_dirs[exercise_files.dir_ind]; dir.init_on_disk()?; WriteStrategy::Overwrite.write(path, exercise_files.exercise) @@ -109,8 +109,8 @@ impl EmbeddedFiles { exercise_ind: usize, exercise_name: &str, ) -> Result { - let exercise_files = &EMBEDDED_FILES.exercise_files[exercise_ind]; - let dir = &EMBEDDED_FILES.exercise_dirs[exercise_files.dir_ind]; + let exercise_files = &self.exercise_files[exercise_ind]; + let dir = &self.exercise_dirs[exercise_files.dir_ind]; // 14 = 10 + 1 + 3 // solutions/ + / + .rs diff --git a/src/exercise.rs b/src/exercise.rs index 4edf378e..6e1b3f0d 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -142,7 +142,7 @@ impl From for Exercise { let hint = exercise_info.hint.trim().to_owned(); - Exercise { + Self { dir, name, path, From 5a1d95028c8d8bca49efc1cc27c4cf6254abf87d Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 17:14:00 +0200 Subject: [PATCH 299/433] Update version in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6ea8de8..c0de89d3 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The following command will download and compile Rustlings: ```bash -cargo install rustlings@6.0.0-beta.8 +cargo install rustlings@6.0.0-beta.9 ```
@@ -44,7 +44,7 @@ cargo install rustlings@6.0.0-beta.8 - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.8 --locked` +- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.9 --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
From 0add5ac240909e2744b492b41a310c9598c1e622 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 17:14:11 +0200 Subject: [PATCH 300/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c1d1a6b..20a628c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -656,7 +656,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rustlings" -version = "6.0.0-beta.8" +version = "6.0.0-beta.9" dependencies = [ "anyhow", "assert_cmd", @@ -675,7 +675,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.8" +version = "6.0.0-beta.9" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index e5525901..508fdcc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.8" +version = "6.0.0-beta.9" authors = [ "Liv ", "Mo Bitar ", @@ -53,7 +53,7 @@ hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.8" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" } serde_json = "1.0.117" serde.workspace = true toml_edit.workspace = true From 2dfc7cdb1a26f46c5537e10e8a182dd2125758cb Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 21:07:04 +0200 Subject: [PATCH 301/433] Document embedded --- src/embedded.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/embedded.rs b/src/embedded.rs index 39ade17d..bc1a5cc6 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -6,6 +6,7 @@ use std::{ use crate::info_file::ExerciseInfo; +// Contains all embedded files. pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); #[derive(Clone, Copy)] @@ -31,12 +32,17 @@ impl WriteStrategy { } } +// Files related to one exercise. struct ExerciseFiles { + // The content of the exercise file. exercise: &'static [u8], + // The content of the solution file. solution: &'static [u8], + // Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`. dir_ind: usize, } +// A directory in the `exercises/` directory. struct ExerciseDir { name: &'static str, readme: &'static [u8], @@ -63,19 +69,20 @@ impl ExerciseDir { let mut readme_path = dir_path; readme_path.push_str("/README.md"); - WriteStrategy::Overwrite.write(&readme_path, self.readme)?; - - Ok(()) + WriteStrategy::Overwrite.write(&readme_path, self.readme) } } +// All embedded files. pub struct EmbeddedFiles { + // `info.toml` pub info_file: &'static str, exercise_files: &'static [ExerciseFiles], exercise_dirs: &'static [ExerciseDir], } impl EmbeddedFiles { + // Dump all the embedded files of the `exercises/` direcotry. pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { create_dir("exercises").context("Failed to create the directory `exercises`")?; From 39a19f945008ef59af107fe54d9dc62943469c8b Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 21:36:20 +0200 Subject: [PATCH 302/433] Document exercise --- src/app_state.rs | 22 +++++++++++++++++++++- src/exercise.rs | 49 +++++++++++++++--------------------------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 75014ceb..b10ebb5e 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -124,7 +124,27 @@ impl AppState { let exercises = exercise_infos .into_iter() - .map(Exercise::from) + .map(|exercise_info| { + // Leaking to be able to borrow in the watch mode `Table`. + // Leaking is not a problem because the `AppState` instance lives until + // the end of the program. + let path = exercise_info.path().leak(); + let name = exercise_info.name.leak(); + let dir = exercise_info.dir.map(|dir| &*dir.leak()); + + let hint = exercise_info.hint.trim().to_owned(); + + Exercise { + dir, + name, + path, + test: exercise_info.test, + strict_clippy: exercise_info.strict_clippy, + hint, + // Updated in `Self::update_from_file`. + done: false, + } + }) .collect::>(); let mut slf = Self { diff --git a/src/exercise.rs b/src/exercise.rs index 6e1b3f0d..494fc427 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -10,30 +10,33 @@ use std::{ use crate::{ cmd::{run_cmd, CargoCmd}, in_official_repo, - info_file::ExerciseInfo, terminal_link::TerminalFileLink, DEBUG_PROFILE, }; +// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; pub struct Exercise { + /// Directory name. pub dir: Option<&'static str>, - // Exercise's unique name + /// Exercise's unique name. pub name: &'static str, - // Exercise's path + /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, pub test: bool, pub strict_clippy: bool, - // The hint text associated with the exercise pub hint: String, pub done: bool, } impl Exercise { + // Run the exercise's binary and append its output to the `output` buffer. + // Compilation should be done before calling this method. fn run_bin(&self, output: &mut Vec, target_dir: &Path) -> Result { writeln!(output, "{}", "Output".underlined())?; + // 7 = "/debug/".len() let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); bin_path.push(target_dir); @@ -43,18 +46,23 @@ impl Exercise { let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; if !success { + // This output is important to show the user that something went wrong. + // Otherwise, calling something like `exit(1)` in an exercise without further output + // leaves the user confused about why the exercise isn't done yet. writeln!( output, "{}", "The exercise didn't run successfully (nonzero exit code)" .bold() - .red() + .red(), )?; } Ok(success) } + /// Compile, check and run the exercise. + /// The output is written to the `output` buffer after clearing it. pub fn run(&self, output: &mut Vec, target_dir: &Path) -> Result { output.clear(); @@ -76,9 +84,10 @@ impl Exercise { return Ok(false); } - // Discard the output of `cargo build` because it will be shown again by the Cargo command. + // Discard the output of `cargo build` because it will be shown again by Clippy. output.clear(); + // `--profile test` is required to also check code with `[cfg(test)]`. let clippy_args: &[&str] = if self.strict_clippy { &["--profile", "test", "--", "-D", "warnings"] } else { @@ -126,34 +135,6 @@ impl Exercise { } } -impl From for Exercise { - fn from(mut exercise_info: ExerciseInfo) -> Self { - // Leaking to be able to borrow in the watch mode `Table`. - // Leaking is not a problem because the `AppState` instance lives until - // the end of the program. - let path = exercise_info.path().leak(); - - exercise_info.name.shrink_to_fit(); - let name = exercise_info.name.leak(); - let dir = exercise_info.dir.map(|mut dir| { - dir.shrink_to_fit(); - &*dir.leak() - }); - - let hint = exercise_info.hint.trim().to_owned(); - - Self { - dir, - name, - path, - test: exercise_info.test, - strict_clippy: exercise_info.strict_clippy, - hint, - done: false, - } - } -} - impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { self.path.fmt(f) From d48e86b1540dcf649412c088cc50161f3e356e26 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 21:40:40 +0200 Subject: [PATCH 303/433] Use public comments for public items --- src/app_state.rs | 10 +++++----- src/cargo_toml.rs | 14 +++++++------- src/cmd.rs | 14 +++++++------- src/embedded.rs | 10 +++++----- src/exercise.rs | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index b10ebb5e..c7c090f6 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -330,8 +330,8 @@ impl AppState { } } - // Official exercises: Dump the solution file form the binary and return its path. - // Third-party exercises: Check if a solution file exists and return its path in that case. + /// Official exercises: Dump the solution file form the binary and return its path. + /// Third-party exercises: Check if a solution file exists and return its path in that case. pub fn current_solution_path(&self) -> Result> { if DEBUG_PROFILE { return Ok(None); @@ -358,9 +358,9 @@ impl AppState { } } - // Mark the current exercise as done and move on to the next pending exercise if one exists. - // If all exercises are marked as done, run all of them to make sure that they are actually - // done. If an exercise which is marked as done fails, mark it as pending and continue on it. + /// Mark the current exercise as done and move on to the next pending exercise if one exists. + /// If all exercises are marked as done, run all of them to make sure that they are actually + /// done. If an exercise which is marked as done fails, mark it as pending and continue on it. pub fn done_current_exercise(&mut self, writer: &mut StdoutLock) -> Result { let exercise = &mut self.exercises[self.current_exercise_ind]; if !exercise.done { diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 106e6a7a..b7951f6b 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -2,10 +2,10 @@ use anyhow::{Context, Result}; use crate::info_file::ExerciseInfo; -// Return the start and end index of the content of the list `bin = […]`. -// bin = [xxxxxxxxxxxxxxxxx] -// |start_ind | -// |end_ind +/// Return the start and end index of the content of the list `bin = […]`. +/// bin = [xxxxxxxxxxxxxxxxx] +/// |start_ind | +/// |end_ind pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { let start_ind = cargo_toml .find("bin = [") @@ -20,8 +20,8 @@ pub fn bins_start_end_ind(cargo_toml: &str) -> Result<(usize, usize)> { Ok((start_ind, end_ind)) } -// Generate and append the content of the `bin` list in `Cargo.toml`. -// The `exercise_path_prefix` is the prefix of the `path` field of every list entry. +/// Generate and append the content of the `bin` list in `Cargo.toml`. +/// The `exercise_path_prefix` is the prefix of the `path` field of every list entry. pub fn append_bins( buf: &mut Vec, exercise_infos: &[ExerciseInfo], @@ -43,7 +43,7 @@ pub fn append_bins( } } -// Update the `bin` list and leave everything else unchanged. +/// Update the `bin` list and leave everything else unchanged. pub fn updated_cargo_toml( exercise_infos: &[ExerciseInfo], current_cargo_toml: &str, diff --git a/src/cmd.rs b/src/cmd.rs index 9762cf85..b914ed88 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1,8 +1,8 @@ use anyhow::{Context, Result}; use std::{io::Read, path::Path, process::Command}; -// Run a command with a description for a possible error and append the merged stdout and stderr. -// The boolean in the returned `Result` is true if the command's exit status is success. +/// Run a command with a description for a possible error and append the merged stdout and stderr. +/// The boolean in the returned `Result` is true if the command's exit status is success. pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { let (mut reader, writer) = os_pipe::pipe() .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; @@ -37,18 +37,18 @@ pub struct CargoCmd<'a> { pub args: &'a [&'a str], pub exercise_name: &'a str, pub description: &'a str, - // RUSTFLAGS="-A warnings" + /// RUSTFLAGS="-A warnings" pub hide_warnings: bool, - // Added as `--target-dir` if `Self::dev` is true. + /// Added as `--target-dir` if `Self::dev` is true. pub target_dir: &'a Path, - // The output buffer to append the merged stdout and stderr. + /// The output buffer to append the merged stdout and stderr. pub output: &'a mut Vec, - // true while developing Rustlings. + /// true while developing Rustlings. pub dev: bool, } impl<'a> CargoCmd<'a> { - // Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. + /// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. pub fn run(&mut self) -> Result { let mut cmd = Command::new("cargo"); cmd.arg(self.subcommand); diff --git a/src/embedded.rs b/src/embedded.rs index bc1a5cc6..6f870684 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -6,7 +6,7 @@ use std::{ use crate::info_file::ExerciseInfo; -// Contains all embedded files. +/// Contains all embedded files. pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); #[derive(Clone, Copy)] @@ -73,16 +73,16 @@ impl ExerciseDir { } } -// All embedded files. +/// All embedded files. pub struct EmbeddedFiles { - // `info.toml` + /// The content of the `info.toml` file. pub info_file: &'static str, exercise_files: &'static [ExerciseFiles], exercise_dirs: &'static [ExerciseDir], } impl EmbeddedFiles { - // Dump all the embedded files of the `exercises/` direcotry. + /// Dump all the embedded files of the `exercises/` direcotry. pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { create_dir("exercises").context("Failed to create the directory `exercises`")?; @@ -110,7 +110,7 @@ impl EmbeddedFiles { WriteStrategy::Overwrite.write(path, exercise_files.exercise) } - // Write the solution file to disk and return its path. + /// Write the solution file to disk and return its path. pub fn write_solution_to_disk( &self, exercise_ind: usize, diff --git a/src/exercise.rs b/src/exercise.rs index 494fc427..a63c9aac 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -14,7 +14,7 @@ use crate::{ DEBUG_PROFILE, }; -// The initial capacity of the output buffer. +/// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; pub struct Exercise { From a67e63cce0443a2a289fdfc275a41cff704cd35e Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 13 May 2024 22:02:45 +0200 Subject: [PATCH 304/433] Document info_file --- src/exercise.rs | 3 +-- src/info_file.rs | 43 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/exercise.rs b/src/exercise.rs index a63c9aac..4bc37cd7 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -17,10 +17,9 @@ use crate::{ /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; +/// See `info_file::ExerciseInfo` pub struct Exercise { - /// Directory name. pub dir: Option<&'static str>, - /// Exercise's unique name. pub name: &'static str, /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, diff --git a/src/info_file.rs b/src/info_file.rs index 14b886b2..0c459284 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -4,44 +4,69 @@ use std::{fs, io::ErrorKind}; use crate::embedded::EMBEDDED_FILES; -// Deserialized from the `info.toml` file. +/// Deserialized from the `info.toml` file. #[derive(Deserialize)] pub struct ExerciseInfo { - // Name of the exercise + /// Exercise's unique name. pub name: String, - // The exercise's directory inside the `exercises` directory + /// Exercise's directory name inside the `exercises/` directory. pub dir: Option, #[serde(default = "default_true")] + /// Run `cargo test` on the exercise. pub test: bool, + /// Deny all Clippy warnings. #[serde(default)] pub strict_clippy: bool, - // The hint text associated with the exercise + /// The exercise's hint to be shown to the user on request. pub hint: String, } -#[inline] +#[inline(always)] const fn default_true() -> bool { true } impl ExerciseInfo { + /// Path to the exercise file starting with the `exercises/` directory. pub fn path(&self) -> String { - if let Some(dir) = &self.dir { - format!("exercises/{dir}/{}.rs", self.name) + let mut path = if let Some(dir) = &self.dir { + // 14 = 10 + 1 + 3 + // exercises/ + / + .rs + let mut path = String::with_capacity(14 + dir.len() + self.name.len()); + path.push_str("exercises/"); + path.push_str(dir); + path.push('/'); + path } else { - format!("exercises/{}.rs", self.name) - } + // 13 = 10 + 3 + // exercises/ + .rs + let mut path = String::with_capacity(13 + self.name.len()); + path.push_str("exercises/"); + path + }; + + path.push_str(&self.name); + path.push_str(".rs"); + + path } } +/// The deserialized `info.toml` file. #[derive(Deserialize)] pub struct InfoFile { + /// For possible breaking changes in the future for third-party exercises. pub format_version: u8, + /// Shown to users when starting with the exercises. pub welcome_message: Option, + /// Shown to users after finishing all exercises. pub final_message: Option, + /// List of all exercises. pub exercises: Vec, } impl InfoFile { + /// Official exercises: Parse the embedded `info.toml` file. + /// Third-party exercises: Parse the `info.toml` file in the current directory. pub fn parse() -> Result { // Read a local `info.toml` if it exists. let slf = match fs::read_to_string("info.toml") { From 700605ff356f70b840f05664b8823a7e14702f92 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 00:35:12 +0200 Subject: [PATCH 305/433] Document init --- src/init.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/init.rs b/src/init.rs index cb3a6bc6..67d8a243 100644 --- a/src/init.rs +++ b/src/init.rs @@ -11,8 +11,11 @@ use std::{ use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile}; pub fn init() -> Result<()> { - if Path::new("exercises").is_dir() && Path::new("Cargo.toml").is_file() { - bail!(PROBABLY_IN_RUSTLINGS_DIR_ERR); + // Prevent initialization in a directory that contains the file `Cargo.toml`. + // This can mean that Rustlings was already initialized in this directory. + // Otherwise, this can cause problems with Cargo workspaces. + if Path::new("Cargo.toml").exists() { + bail!(CARGO_TOML_EXISTS_ERR); } let rustlings_path = Path::new("rustlings"); @@ -24,7 +27,7 @@ pub fn init() -> Result<()> { } set_current_dir("rustlings") - .context("Failed to change the current directory to `rustlings`")?; + .context("Failed to change the current directory to `rustlings/`")?; let info_file = InfoFile::parse()?; EMBEDDED_FILES @@ -37,9 +40,10 @@ pub fn init() -> Result<()> { .as_bytes() .iter() .position(|c| *c == b'\n') - .context("The embedded `Cargo.toml` is empty or contains only one line.")?; - let current_cargo_toml = - ¤t_cargo_toml[(newline_ind + 1).min(current_cargo_toml.len() - 1)..]; + .context("The embedded `Cargo.toml` is empty or contains only one line")?; + let current_cargo_toml = current_cargo_toml + .get(newline_ind + 1..) + .context("The embedded `Cargo.toml` contains only one line")?; let updated_cargo_toml = updated_cargo_toml(&info_file.exercises, current_cargo_toml, b"") .context("Failed to generate `Cargo.toml`")?; fs::write("Cargo.toml", updated_cargo_toml) @@ -77,12 +81,10 @@ target pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; -const PROBABLY_IN_RUSTLINGS_DIR_ERR: &str = - "A directory with the name `exercises` and a file with the name `Cargo.toml` already exist -in the current directory. It looks like Rustlings was already initialized here. -Run `rustlings` for instructions on getting started with the exercises. +const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`. -If you didn't already initialize Rustlings, please initialize it in another directory."; +If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises. +Otherwise, please run `rustlings init` again in another directory."; const RUSTLINGS_DIR_ALREADY_EXISTS_ERR: &str = "A directory with the name `rustlings` already exists in the current directory. From 0ae66d18607a78c1dc55879cafd70732604e528e Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 00:55:07 +0200 Subject: [PATCH 306/433] Remove inline --- src/list/state.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/list/state.rs b/src/list/state.rs index 0f2a1c82..d6df6344 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -148,14 +148,12 @@ impl<'a> UiState<'a> { } } - #[inline] pub fn select_first(&mut self) { if self.n_rows > 0 { self.table_state.select(Some(0)); } } - #[inline] pub fn select_last(&mut self) { if self.n_rows > 0 { self.table_state.select(Some(self.n_rows - 1)); From 96a44f3dcf2dd9e2562b757d7840084b45b90b61 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 01:23:58 +0200 Subject: [PATCH 307/433] Make it more clear that only one char is expected --- src/watch/state.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index 60b6d5a8..abd21fbd 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -96,18 +96,18 @@ impl<'a> WatchState<'a> { self.writer.write_all(b"\n")?; if self.manual_run { - write!(self.writer, "{}un/", 'r'.bold())?; + write!(self.writer, "{}:run / ", 'r'.bold())?; } if self.done_status != DoneStatus::Pending { - write!(self.writer, "{}ext/", 'n'.bold())?; + write!(self.writer, "{}:next / ", 'n'.bold())?; } if !self.show_hint { - write!(self.writer, "{}int/", 'h'.bold())?; + write!(self.writer, "{}:hint / ", 'h'.bold())?; } - write!(self.writer, "{}ist/{}uit? ", 'l'.bold(), 'q'.bold())?; + write!(self.writer, "{}:list / {}:quit ? ", 'l'.bold(), 'q'.bold())?; self.writer.flush() } From c8481d35c120ff99213e6ed73ba889e51cac10c5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 01:49:22 +0200 Subject: [PATCH 308/433] Done documentation --- src/progress_bar.rs | 3 +++ src/run.rs | 6 +----- src/watch.rs | 5 +++-- src/watch/notify_event.rs | 5 +++-- src/watch/state.rs | 9 +++++---- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/progress_bar.rs b/src/progress_bar.rs index d6962b8c..4a54170a 100644 --- a/src/progress_bar.rs +++ b/src/progress_bar.rs @@ -12,6 +12,7 @@ const MIN_LINE_WIDTH: u16 = WRAPPER_WIDTH + 4; const PROGRESS_EXCEEDS_MAX_ERR: &str = "The progress of the progress bar is higher than the maximum"; +/// Terminal progress bar to be used when not using Ratataui. pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result { use crossterm::style::Stylize; @@ -54,6 +55,8 @@ pub fn progress_bar(progress: u16, total: u16, line_width: u16) -> Result Result> { use ratatui::style::Stylize; diff --git a/src/run.rs b/src/run.rs index ac974143..36899b91 100644 --- a/src/run.rs +++ b/src/run.rs @@ -41,11 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> { match app_state.done_current_exercise(&mut stdout)? { ExercisesProgress::AllDone => (), - ExercisesProgress::CurrentPending => println!( - "Current exercise: {}", - app_state.current_exercise().terminal_link(), - ), - ExercisesProgress::NewPending => println!( + ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!( "Next exercise: {}", app_state.current_exercise().terminal_link(), ), diff --git a/src/watch.rs b/src/watch.rs index 2fbc5337..88a12301 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -14,7 +14,7 @@ use std::{ use crate::app_state::{AppState, ExercisesProgress}; use self::{ - notify_event::DebounceEventHandler, + notify_event::NotifyEventHandler, state::WatchState, terminal_event::{terminal_event_handler, InputEvent}, }; @@ -40,6 +40,7 @@ pub enum WatchExit { List, } +/// `notify_exercise_names` as None activates the manual run mode. pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, @@ -52,7 +53,7 @@ pub fn watch( let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let mut debouncer = new_debouncer( Duration::from_millis(200), - DebounceEventHandler { + NotifyEventHandler { tx: tx.clone(), exercise_names, }, diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index a2243771..74716409 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -3,12 +3,13 @@ use std::sync::mpsc::Sender; use super::WatchEvent; -pub struct DebounceEventHandler { +pub struct NotifyEventHandler { pub tx: Sender, + /// Used to report which exercise was modified. pub exercise_names: &'static [&'static [u8]], } -impl notify_debouncer_mini::DebounceEventHandler for DebounceEventHandler { +impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { fn handle_event(&mut self, input_event: DebounceEventResult) { let output_event = match input_event { Ok(input_event) => { diff --git a/src/watch/state.rs b/src/watch/state.rs index abd21fbd..14c3f015 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -1,7 +1,7 @@ use anyhow::Result; use crossterm::{ style::{style, Stylize}, - terminal::size, + terminal, }; use std::io::{self, StdoutLock, Write}; @@ -84,6 +84,7 @@ impl<'a> WatchState<'a> { self.run_current_exercise() } + /// Move on to the next exercise if the current one is done. pub fn next_exercise(&mut self) -> Result { if self.done_status == DoneStatus::Pending { return Ok(ExercisesProgress::CurrentPending); @@ -113,7 +114,7 @@ impl<'a> WatchState<'a> { } pub fn render(&mut self) -> Result<()> { - // Prevent having the first line shifted. + // Prevent having the first line shifted if clearing wasn't successful. self.writer.write_all(b"\n")?; clear_terminal(&mut self.writer)?; @@ -145,11 +146,11 @@ When you are done experimenting, enter `n` to move on to the next exercise 🦀" writeln!( self.writer, "A solution file can be found at {}\n", - style(TerminalFileLink(solution_path)).underlined().green() + style(TerminalFileLink(solution_path)).underlined().green(), )?; } - let line_width = size()?.0; + let line_width = terminal::size()?.0; let progress_bar = progress_bar( self.app_state.n_done(), self.app_state.exercises().len() as u16, From cf3f6fd6a16e81905bb44676d623502aeb8e5d01 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 14 May 2024 01:50:03 +0200 Subject: [PATCH 309/433] Fix typo --- src/embedded.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embedded.rs b/src/embedded.rs index 6f870684..e710a4e5 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -82,7 +82,7 @@ pub struct EmbeddedFiles { } impl EmbeddedFiles { - /// Dump all the embedded files of the `exercises/` direcotry. + /// Dump all the embedded files of the `exercises/` directory. pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { create_dir("exercises").context("Failed to create the directory `exercises`")?; From bde2524c3b1043da489541d4885592abe16646fa Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 20 May 2024 18:11:19 +0200 Subject: [PATCH 310/433] Update deps --- Cargo.lock | 74 ++++++++++++++++++++++++++++-------------------------- Cargo.toml | 8 +++--- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20a628c7..10dfa306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "assert_cmd" @@ -210,18 +210,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -262,9 +262,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "equivalent" @@ -334,12 +334,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "indoc" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" - [[package]] name = "inotify" version = "0.9.6" @@ -403,9 +397,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "lock_api" @@ -571,9 +565,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" dependencies = [ "unicode-ident", ] @@ -589,21 +583,21 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" dependencies = [ "bitflags 2.5.0", "cassowary", "compact_str", "crossterm", - "indoc", "itertools", "lru", "paste", "stability", "strum", "unicode-segmentation", + "unicode-truncate", "unicode-width", ] @@ -684,9 +678,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -711,18 +705,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.201" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", @@ -742,9 +736,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -831,9 +825,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.63" +version = "2.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" dependencies = [ "proc-macro2", "quote", @@ -848,18 +842,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" dependencies = [ "indexmap", "serde", @@ -880,6 +874,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools", + "unicode-width", +] + [[package]] name = "unicode-width" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 508fdcc3..ebcbe6cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ license = "MIT" edition = "2021" [workspace.dependencies] -serde = { version = "1.0.199", features = ["derive"] } -toml_edit = { version = "0.22.12", default-features = false, features = ["parse", "serde"] } +serde = { version = "1.0.202", features = ["derive"] } +toml_edit = { version = "0.22.13", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" @@ -46,13 +46,13 @@ include = [ ] [dependencies] -anyhow = "1.0.83" +anyhow = "1.0.86" clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.1.5" -ratatui = { version = "0.26.2", default-features = false, features = ["crossterm"] } +ratatui = { version = "0.26.3", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" } serde_json = "1.0.117" serde.workspace = true From 0f4c42d54ea7322a4ee0ae7036c058c3061e80e9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 21 May 2024 01:47:57 +0200 Subject: [PATCH 311/433] Add solutions to intro and variables --- exercises/00_intro/intro2.rs | 4 +-- exercises/01_variables/variables1.rs | 6 ++--- exercises/01_variables/variables2.rs | 2 ++ exercises/01_variables/variables3.rs | 4 ++- exercises/01_variables/variables4.rs | 9 ++++--- exercises/01_variables/variables5.rs | 10 +++++--- exercises/01_variables/variables6.rs | 4 ++- rustlings-macros/info.toml | 37 ++++++++++++++-------------- solutions/00_intro/intro1.rs | 3 ++- solutions/00_intro/intro2.rs | 5 +++- solutions/01_variables/variables1.rs | 7 +++++- solutions/01_variables/variables2.rs | 17 ++++++++++++- solutions/01_variables/variables3.rs | 14 ++++++++++- solutions/01_variables/variables4.rs | 10 +++++++- solutions/01_variables/variables5.rs | 10 +++++++- solutions/01_variables/variables6.rs | 7 +++++- 16 files changed, 109 insertions(+), 40 deletions(-) diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs index c7a3ab2a..e443ec8f 100644 --- a/exercises/00_intro/intro2.rs +++ b/exercises/00_intro/intro2.rs @@ -1,5 +1,5 @@ -// Make the code print a greeting to the world. +// TODO: Fix the code to print "Hello world!". fn main() { - printline!("Hello there!") + printline!("Hello world!"); } diff --git a/exercises/01_variables/variables1.rs b/exercises/01_variables/variables1.rs index 3035bfae..0a9e5548 100644 --- a/exercises/01_variables/variables1.rs +++ b/exercises/01_variables/variables1.rs @@ -1,6 +1,6 @@ -// Make me compile! - fn main() { + // TODO: Add missing keyword. x = 5; - println!("x has the value {}", x); + + println!("x has the value {x}"); } diff --git a/exercises/01_variables/variables2.rs b/exercises/01_variables/variables2.rs index ce2dd851..e2a36035 100644 --- a/exercises/01_variables/variables2.rs +++ b/exercises/01_variables/variables2.rs @@ -1,5 +1,7 @@ fn main() { + // TODO: Change the line below to fix the compiler error. let x; + if x == 10 { println!("x is ten!"); } else { diff --git a/exercises/01_variables/variables3.rs b/exercises/01_variables/variables3.rs index 488385ba..06f35bb1 100644 --- a/exercises/01_variables/variables3.rs +++ b/exercises/01_variables/variables3.rs @@ -1,4 +1,6 @@ fn main() { + // TODO: Change the line below to fix the compiler error. let x: i32; - println!("Number {}", x); + + println!("Number {x}"); } diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs index 67be1271..8634ceb5 100644 --- a/exercises/01_variables/variables4.rs +++ b/exercises/01_variables/variables4.rs @@ -1,6 +1,9 @@ +// TODO: Fix the compiler error. + fn main() { let x = 3; - println!("Number {}", x); - x = 5; // don't change this line - println!("Number {}", x); + println!("Number {x}"); + + x = 5; // Don't change this line + println!("Number {x}"); } diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index 3a745411..73f655e3 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -1,6 +1,8 @@ fn main() { - let number = "T-H-R-E-E"; // don't change this line - println!("Spell a Number : {}", number); - number = 3; // don't rename this variable - println!("Number plus two is : {}", number + 2); + let number = "T-H-R-E-E"; // Don't change this line + println!("Spell a number: {}", number); + + // TODO: Fix the compiler error by changing the line below without renaming the the variable. + number = 3; + println!("Number plus two is: {}", number + 2); } diff --git a/exercises/01_variables/variables6.rs b/exercises/01_variables/variables6.rs index 4746331b..4a040fdd 100644 --- a/exercises/01_variables/variables6.rs +++ b/exercises/01_variables/variables6.rs @@ -1,4 +1,6 @@ +// TODO: Change the line below to fix the compiler error. const NUMBER = 3; + fn main() { - println!("Number {}", NUMBER); + println!("Number: {NUMBER}"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 485665e4..be3b262d 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -29,20 +29,21 @@ https://github.com/rust-lang/rustlings/blob/main/CONTRIBUTING.md # INTRO -# TODO: Update exercise [[exercises]] name = "intro1" dir = "00_intro" test = false -# TODO: Fix hint -hint = """Enter `n` to move on to the next exercise. You might need to press ENTER after typing `n`.""" +hint = """ +Enter `n` to move on to the next exercise. +You might need to press ENTER after typing `n`.""" [[exercises]] name = "intro2" dir = "00_intro" test = false hint = """ -The compiler is informing us that we've got the name of the print macro wrong, and has suggested an alternative.""" +The compiler is informing us that we've got the name of the print macro wrong. +It also suggests an alternative.""" # VARIABLES @@ -51,18 +52,18 @@ name = "variables1" dir = "01_variables" test = false hint = """ -The declaration in the first line in the main function is missing a keyword -that is needed in Rust to create a new variable binding.""" +The declaration in the `main` function is missing a keyword that is needed +in Rust to create a new variable binding.""" [[exercises]] name = "variables2" dir = "01_variables" test = false hint = """ -The compiler message is saying that Rust cannot infer the type that the +The compiler message is saying that Rust can't infer the type that the variable binding `x` has with what is given here. -What happens if you annotate the first line in the main function with a type +What happens if you annotate the first line in the `main` function with a type annotation? What if you give `x` a value? @@ -78,9 +79,9 @@ name = "variables3" dir = "01_variables" test = false hint = """ -Oops! In this exercise, we have a variable binding that we've created on in the -first line in the `main` function, and we're trying to use it in the next line, -but we haven't given it a value. +In this exercise, we have a variable binding that we've created in the `main` +function, and we're trying to use it in the next line, but we haven't given it +a value. We can't print out something that isn't there; try giving `x` a value! @@ -92,7 +93,7 @@ name = "variables4" dir = "01_variables" test = false hint = """ -In Rust, variable bindings are immutable by default. But here we're trying +In Rust, variable bindings are immutable by default. But here, we're trying to reassign a different value to `x`! There's a keyword we can use to make a variable binding mutable instead.""" @@ -120,12 +121,12 @@ dir = "01_variables" test = false hint = """ We know about variables and mutability, but there is another important type of -variable available: constants. +variables available: constants. -Constants are always immutable and they are declared with keyword `const` rather -than keyword `let`. +Constants are always immutable. They are declared with the keyword `const` instead +of `let`. -Constants types must also always be annotated. +The type of Constants must always be annotated. Read more about constants and the differences between variables and constants under 'Constants' in the book's section 'Variables and Mutability': @@ -139,7 +140,7 @@ name = "functions1" dir = "02_functions" test = false hint = """ -This main function is calling a function that it expects to exist, but the +This `main` function is calling a function that it expects to exist, but the function doesn't exist. It expects this function to have the name `call_me`. It expects this function to not take any arguments and not return a value. Sounds a lot like `main`, doesn't it?""" @@ -688,7 +689,7 @@ test = false hint = """ If other functions can return a `Result`, why shouldn't `main`? It's a fairly common convention to return something like `Result<(), ErrorType>` from your -main function. +`main` function. The unit (`()`) type is there because nothing is really needed in terms of positive results.""" diff --git a/solutions/00_intro/intro1.rs b/solutions/00_intro/intro1.rs index 4e181989..07d4e4fd 100644 --- a/solutions/00_intro/intro1.rs +++ b/solutions/00_intro/intro1.rs @@ -1 +1,2 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The exercise `intro1` only requires entering `n` in the terminal to go to the next exercise. +// It is just an introduction to how Rustlings works. diff --git a/solutions/00_intro/intro2.rs b/solutions/00_intro/intro2.rs index 4e181989..b8e031a0 100644 --- a/solutions/00_intro/intro2.rs +++ b/solutions/00_intro/intro2.rs @@ -1 +1,4 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // `println!` instead of `printline!`. + println!("Hello world!"); +} diff --git a/solutions/01_variables/variables1.rs b/solutions/01_variables/variables1.rs index 4e181989..58d046b2 100644 --- a/solutions/01_variables/variables1.rs +++ b/solutions/01_variables/variables1.rs @@ -1 +1,6 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // Declaring variables requires the `let` keyword. + let x = 5; + + println!("x has the value {x}"); +} diff --git a/solutions/01_variables/variables2.rs b/solutions/01_variables/variables2.rs index 4e181989..50b8d1b4 100644 --- a/solutions/01_variables/variables2.rs +++ b/solutions/01_variables/variables2.rs @@ -1 +1,16 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // The easiest way to fix the compiler error is to initialize the + // variable `x`. By setting its value to an integer, Rust infers its type + // as `i32` which is the default type for integers. + let x = 42; + + // But we can enforce a type different from the default `i32` by adding + // a type annotation: + // let x: u8 = 42; + + if x == 10 { + println!("x is ten!"); + } else { + println!("x is not ten!"); + } +} diff --git a/solutions/01_variables/variables3.rs b/solutions/01_variables/variables3.rs index 4e181989..7db42a95 100644 --- a/solutions/01_variables/variables3.rs +++ b/solutions/01_variables/variables3.rs @@ -1 +1,13 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // Reading uninitialized variables isn't allowed in Rust! + // Therefore, we need to assign a value first. + let x: i32 = 42; + + println!("Number {x}"); + + // It possible to declare a variable and initialize it later. + // But it can't be used before initialization. + let y: i32; + y = 42; + println!("Number {y}"); +} diff --git a/solutions/01_variables/variables4.rs b/solutions/01_variables/variables4.rs index 4e181989..0540caa2 100644 --- a/solutions/01_variables/variables4.rs +++ b/solutions/01_variables/variables4.rs @@ -1 +1,9 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // In Rust, variables are immutable by default. + // Adding the `mut` keyword after `let` makes the declared variable mutable. + let mut x = 3; + println!("Number {x}"); + + x = 5; // Don't change this line + println!("Number {x}"); +} diff --git a/solutions/01_variables/variables5.rs b/solutions/01_variables/variables5.rs index 4e181989..456dc9cf 100644 --- a/solutions/01_variables/variables5.rs +++ b/solutions/01_variables/variables5.rs @@ -1 +1,9 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + let number = "T-H-R-E-E"; // Don't change this line + println!("Spell a number: {}", number); + + // Using variable shadowing + // https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing + let number = 3; + println!("Number plus two is: {}", number + 2); +} diff --git a/solutions/01_variables/variables6.rs b/solutions/01_variables/variables6.rs index 4e181989..25b7a1e4 100644 --- a/solutions/01_variables/variables6.rs +++ b/solutions/01_variables/variables6.rs @@ -1 +1,6 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The type of constants must always be annotated. +const NUMBER: u64 = 3; + +fn main() { + println!("Number: {NUMBER}"); +} From d0b843d6c4a99636d3dc6caf3ceebea14cb3b07d Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 21 May 2024 02:43:18 +0200 Subject: [PATCH 312/433] Add solutions to functions --- exercises/02_functions/functions1.rs | 4 +++- exercises/02_functions/functions2.rs | 9 +++++---- exercises/02_functions/functions3.rs | 9 +++++---- exercises/02_functions/functions4.rs | 19 ++++++++++--------- exercises/02_functions/functions5.rs | 11 ++++++----- rustlings-macros/info.toml | 22 +++++++++++----------- solutions/01_variables/variables4.rs | 2 +- solutions/01_variables/variables5.rs | 2 +- solutions/02_functions/functions1.rs | 9 ++++++++- solutions/02_functions/functions2.rs | 12 +++++++++++- solutions/02_functions/functions3.rs | 11 ++++++++++- solutions/02_functions/functions4.rs | 18 +++++++++++++++++- solutions/02_functions/functions5.rs | 10 +++++++++- 13 files changed, 97 insertions(+), 41 deletions(-) diff --git a/exercises/02_functions/functions1.rs b/exercises/02_functions/functions1.rs index 4e3b1036..a812c21b 100644 --- a/exercises/02_functions/functions1.rs +++ b/exercises/02_functions/functions1.rs @@ -1,3 +1,5 @@ +// TODO: Add some function with the name `call_me` without arguments or a return value. + fn main() { - call_me(); + call_me(); // Don't change this line } diff --git a/exercises/02_functions/functions2.rs b/exercises/02_functions/functions2.rs index 84e09cda..2c773c6b 100644 --- a/exercises/02_functions/functions2.rs +++ b/exercises/02_functions/functions2.rs @@ -1,9 +1,10 @@ -fn main() { - call_me(3); -} - +// TODO: Add the missing type of the argument `num` after the colon `:`. fn call_me(num:) { for i in 0..num { println!("Ring! Call number {}", i + 1); } } + +fn main() { + call_me(3); +} diff --git a/exercises/02_functions/functions3.rs b/exercises/02_functions/functions3.rs index 66fb6d32..5d5122af 100644 --- a/exercises/02_functions/functions3.rs +++ b/exercises/02_functions/functions3.rs @@ -1,9 +1,10 @@ -fn main() { - call_me(); -} - fn call_me(num: u32) { for i in 0..num { println!("Ring! Call number {}", i + 1); } } + +fn main() { + // TODO: Fix the function call. + call_me(); +} diff --git a/exercises/02_functions/functions4.rs b/exercises/02_functions/functions4.rs index 06d3b1bb..b22bffda 100644 --- a/exercises/02_functions/functions4.rs +++ b/exercises/02_functions/functions4.rs @@ -1,14 +1,14 @@ // This store is having a sale where if the price is an even number, you get 10 -// Rustbucks off, but if it's an odd number, it's 3 Rustbucks off. (Don't worry -// about the function bodies themselves, we're only interested in the signatures -// for now. If anything, this is a good way to peek ahead to future exercises!) +// Rustbucks off, but if it's an odd number, it's 3 Rustbucks off. +// Don't worry about the function bodies themselves, we are only interested in +// the signatures for now. -fn main() { - let original_price = 51; - println!("Your sale price is {}", sale_price(original_price)); +fn is_even(num: i64) -> bool { + num % 2 == 0 } -fn sale_price(price: i32) -> { +// TODO: Fix the function signature. +fn sale_price(price: i64) -> { if is_even(price) { price - 10 } else { @@ -16,6 +16,7 @@ fn sale_price(price: i32) -> { } } -fn is_even(num: i32) -> bool { - num % 2 == 0 +fn main() { + let original_price = 51; + println!("Your sale price is {}", sale_price(original_price)); } diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs index 3bb5e52a..31fd057a 100644 --- a/exercises/02_functions/functions5.rs +++ b/exercises/02_functions/functions5.rs @@ -1,8 +1,9 @@ -fn main() { - let answer = square(3); - println!("The square of 3 is {}", answer); -} - +// TODO: Fix the function body without chaning the signature. fn square(num: i32) -> i32 { num * num; } + +fn main() { + let answer = square(3); + println!("The square of 3 is {answer}"); +} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index be3b262d..495e9c3e 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -142,7 +142,7 @@ test = false hint = """ This `main` function is calling a function that it expects to exist, but the function doesn't exist. It expects this function to have the name `call_me`. -It expects this function to not take any arguments and not return a value. +It also expects this function to not take any arguments and not return a value. Sounds a lot like `main`, doesn't it?""" [[exercises]] @@ -159,7 +159,7 @@ dir = "02_functions" test = false hint = """ This time, the function *declaration* is okay, but there's something wrong -with the place where we're calling the function.""" +with the place where we are calling the function.""" [[exercises]] name = "functions4" @@ -167,8 +167,8 @@ dir = "02_functions" test = false hint = """ The error message points to the function `sale_price` and says it expects a type -after the `->`. This is where the function's return type should be -- take a -look at the `is_even` function for an example!""" +after `->`. This is where the function's return type should be. +Take a look at the `is_even` function for an example!""" [[exercises]] name = "functions5" @@ -177,15 +177,15 @@ test = false hint = """ This is a really common error that can be fixed by removing one character. It happens because Rust distinguishes between expressions and statements: -expressions return a value based on their operand(s), and statements simply -return a `()` type which behaves just like `void` in C/C++ language. +Expressions return a value based on their operand(s), and statements simply +return a `()` type which behaves just like `void` in C/C++. -We want to return a value of `i32` type from the `square` function, but it is -returning a `()` type... +We want to return a value with the type `i32` from the `square` function, but +it is returning the type `()`. -They are not the same. There are two solutions: -1. Add a `return` ahead of `num * num;` -2. remove `;`, make it to be `num * num`""" +There are two solutions: +1. Add the `return` keyword before `num * num;` +2. Remove the semicolon `;` after `num * num`""" # IF diff --git a/solutions/01_variables/variables4.rs b/solutions/01_variables/variables4.rs index 0540caa2..7de6bcbf 100644 --- a/solutions/01_variables/variables4.rs +++ b/solutions/01_variables/variables4.rs @@ -4,6 +4,6 @@ fn main() { let mut x = 3; println!("Number {x}"); - x = 5; // Don't change this line + x = 5; println!("Number {x}"); } diff --git a/solutions/01_variables/variables5.rs b/solutions/01_variables/variables5.rs index 456dc9cf..9057754c 100644 --- a/solutions/01_variables/variables5.rs +++ b/solutions/01_variables/variables5.rs @@ -1,5 +1,5 @@ fn main() { - let number = "T-H-R-E-E"; // Don't change this line + let number = "T-H-R-E-E"; println!("Spell a number: {}", number); // Using variable shadowing diff --git a/solutions/02_functions/functions1.rs b/solutions/02_functions/functions1.rs index 4e181989..dc527446 100644 --- a/solutions/02_functions/functions1.rs +++ b/solutions/02_functions/functions1.rs @@ -1 +1,8 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Some function with the name `call_me` without arguments or a return value. +fn call_me() { + println!("Hello world!"); +} + +fn main() { + call_me(); +} diff --git a/solutions/02_functions/functions2.rs b/solutions/02_functions/functions2.rs index 4e181989..f14ffa35 100644 --- a/solutions/02_functions/functions2.rs +++ b/solutions/02_functions/functions2.rs @@ -1 +1,11 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The type of function arguments must be annotated. +// Added the type annotation `u64`. +fn call_me(num: u64) { + for i in 0..num { + println!("Ring! Call number {}", i + 1); + } +} + +fn main() { + call_me(3); +} diff --git a/solutions/02_functions/functions3.rs b/solutions/02_functions/functions3.rs index 4e181989..c581c425 100644 --- a/solutions/02_functions/functions3.rs +++ b/solutions/02_functions/functions3.rs @@ -1 +1,10 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn call_me(num: u32) { + for i in 0..num { + println!("Ring! Call number {}", i + 1); + } +} + +fn main() { + // `call_me` expects an argument. + call_me(5); +} diff --git a/solutions/02_functions/functions4.rs b/solutions/02_functions/functions4.rs index 4e181989..f823de24 100644 --- a/solutions/02_functions/functions4.rs +++ b/solutions/02_functions/functions4.rs @@ -1 +1,17 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn is_even(num: i64) -> bool { + num % 2 == 0 +} + +// The return type must always be annotated. +fn sale_price(price: i64) -> i64 { + if is_even(price) { + price - 10 + } else { + price - 3 + } +} + +fn main() { + let original_price = 51; + println!("Your sale price is {}", sale_price(original_price)); +} diff --git a/solutions/02_functions/functions5.rs b/solutions/02_functions/functions5.rs index 4e181989..03354186 100644 --- a/solutions/02_functions/functions5.rs +++ b/solutions/02_functions/functions5.rs @@ -1 +1,9 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn square(num: i32) -> i32 { + // Removed the semicolon `;` at the end of the line below to implicitely return the result. + num * num +} + +fn main() { + let answer = square(3); + println!("The square of 3 is {answer}"); +} From 3bb71c6b0c9d58e421f79d914f5483cb5a98af0b Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 22 May 2024 15:04:12 +0200 Subject: [PATCH 313/433] Remove unneeded pub --- exercises/03_if/if1.rs | 4 ++-- exercises/03_if/if2.rs | 2 +- exercises/03_if/if3.rs | 2 +- exercises/13_error_handling/errors1.rs | 2 +- exercises/13_error_handling/errors2.rs | 2 +- exercises/13_error_handling/errors3.rs | 2 +- exercises/14_generics/generics2.rs | 2 +- exercises/15_traits/traits3.rs | 2 +- exercises/15_traits/traits4.rs | 2 +- exercises/15_traits/traits5.rs | 4 ++-- exercises/17_tests/tests3.rs | 2 +- exercises/17_tests/tests4.rs | 2 +- exercises/18_iterators/iterators2.rs | 6 +++--- exercises/18_iterators/iterators3.rs | 6 +++--- exercises/18_iterators/iterators4.rs | 2 +- exercises/19_smart_pointers/box1.rs | 6 +++--- exercises/quizzes/quiz2.rs | 2 +- exercises/quizzes/quiz3.rs | 10 +++++----- 18 files changed, 30 insertions(+), 30 deletions(-) diff --git a/exercises/03_if/if1.rs b/exercises/03_if/if1.rs index 52dee0b5..e5a3c5a5 100644 --- a/exercises/03_if/if1.rs +++ b/exercises/03_if/if1.rs @@ -1,5 +1,5 @@ -pub fn bigger(a: i32, b: i32) -> i32 { - // Complete this function to return the bigger number! +fn bigger(a: i32, b: i32) -> i32 { + // TODO: Complete this function to return the bigger number! // If both numbers are equal, any of them can be returned. // Do not use: // - another function call diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index a06bba55..d834ab27 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -1,7 +1,7 @@ // Step 1: Make me compile! // Step 2: Get the bar_for_fuzz and default_to_baz tests passing! -pub fn foo_if_fizz(fizzish: &str) -> &str { +fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { "foo" } else { diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index 1d9b7c25..ff6fee64 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -1,4 +1,4 @@ -pub fn animal_habitat(animal: &str) -> &'static str { +fn animal_habitat(animal: &str) -> &'static str { let identifier = if animal == "crab" { 1 } else if animal == "gopher" { diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 15a3716d..e3e04823 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -8,7 +8,7 @@ fn main() { // You can optionally experiment here. } -pub fn generate_nametag_text(name: String) -> Option { +fn generate_nametag_text(name: String) -> Option { if name.is_empty() { // Empty names aren't allowed. None diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index e39aa959..345a0eef 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -16,7 +16,7 @@ use std::num::ParseIntError; -pub fn total_cost(item_quantity: &str) -> Result { +fn total_cost(item_quantity: &str) -> Result { let processing_fee = 1; let cost_per_item = 5; let qty = item_quantity.parse::(); diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index 5661f17b..2ef84f98 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -18,7 +18,7 @@ fn main() { } } -pub fn total_cost(item_quantity: &str) -> Result { +fn total_cost(item_quantity: &str) -> Result { let processing_fee = 1; let cost_per_item = 5; let qty = item_quantity.parse::()?; diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index cbb9b5f9..6cdcdaf5 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -6,7 +6,7 @@ struct Wrapper { } impl Wrapper { - pub fn new(value: u32) -> Self { + fn new(value: u32) -> Self { Wrapper { value } } } diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 9a2365ae..66da235f 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -3,7 +3,7 @@ // // Consider what you can add to the Licensed trait. -pub trait Licensed { +trait Licensed { fn licensing_info(&self) -> String; } diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index 7af30b57..ed63f6e1 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -2,7 +2,7 @@ // // Don't change any line other than the marked one. -pub trait Licensed { +trait Licensed { fn licensing_info(&self) -> String { "some information".to_string() } diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs index 9a45bb76..3e62283f 100644 --- a/exercises/15_traits/traits5.rs +++ b/exercises/15_traits/traits5.rs @@ -2,13 +2,13 @@ // // Don't change any line other than the marked one. -pub trait SomeTrait { +trait SomeTrait { fn some_function(&self) -> bool { true } } -pub trait OtherTrait { +trait OtherTrait { fn other_function(&self) -> bool { true } diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index 3b4e1990..d1cb4892 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -2,7 +2,7 @@ // the test passes. Then write a second test that tests whether we get the // result we expect to get when we call `is_even(5)`. -pub fn is_even(num: i32) -> bool { +fn is_even(num: i32) -> bool { num % 2 == 0 } diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs index 35a9a3b5..4303ed06 100644 --- a/exercises/17_tests/tests4.rs +++ b/exercises/17_tests/tests4.rs @@ -7,7 +7,7 @@ struct Rectangle { impl Rectangle { // Only change the test functions themselves - pub fn new(width: i32, height: i32) -> Self { + fn new(width: i32, height: i32) -> Self { if width <= 0 || height <= 0 { panic!("Rectangle width and height cannot be negative!") } diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index df1fa838..8d8909bf 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -4,7 +4,7 @@ // Step 1. // Complete the `capitalize_first` function. // "hello" -> "Hello" -pub fn capitalize_first(input: &str) -> String { +fn capitalize_first(input: &str) -> String { let mut c = input.chars(); match c.next() { None => String::new(), @@ -16,7 +16,7 @@ pub fn capitalize_first(input: &str) -> String { // Apply the `capitalize_first` function to a slice of string slices. // Return a vector of strings. // ["hello", "world"] -> ["Hello", "World"] -pub fn capitalize_words_vector(words: &[&str]) -> Vec { +fn capitalize_words_vector(words: &[&str]) -> Vec { vec![] } @@ -24,7 +24,7 @@ pub fn capitalize_words_vector(words: &[&str]) -> Vec { // Apply the `capitalize_first` function again to a slice of string slices. // Return a single string. // ["hello", " ", "world"] -> "Hello World" -pub fn capitalize_words_string(words: &[&str]) -> String { +fn capitalize_words_string(words: &[&str]) -> String { String::new() } diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index 9f106aa8..dfe40149 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -5,20 +5,20 @@ // list_of_results functions. #[derive(Debug, PartialEq, Eq)] -pub enum DivisionError { +enum DivisionError { NotDivisible(NotDivisibleError), DivideByZero, } #[derive(Debug, PartialEq, Eq)] -pub struct NotDivisibleError { +struct NotDivisibleError { dividend: i32, divisor: i32, } // Calculate `a` divided by `b` if `a` is evenly divisible by `b`. // Otherwise, return a suitable error. -pub fn divide(a: i32, b: i32) -> Result { +fn divide(a: i32, b: i32) -> Result { todo!(); } diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index 60c7b8d1..ae4d502d 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -1,4 +1,4 @@ -pub fn factorial(num: u64) -> u64 { +fn factorial(num: u64) -> u64 { // Complete this function to return the factorial of num // Do not use: // - early returns (using the `return` keyword explicitly) diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index 226a1177..908c9232 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -15,7 +15,7 @@ // Note: the tests should not be changed #[derive(PartialEq, Debug)] -pub enum List { +enum List { Cons(i32, List), Nil, } @@ -28,11 +28,11 @@ fn main() { ); } -pub fn create_empty_list() -> List { +fn create_empty_list() -> List { todo!() } -pub fn create_non_empty_list() -> List { +fn create_non_empty_list() -> List { todo!() } diff --git a/exercises/quizzes/quiz2.rs b/exercises/quizzes/quiz2.rs index 0a29e781..e01e3f1d 100644 --- a/exercises/quizzes/quiz2.rs +++ b/exercises/quizzes/quiz2.rs @@ -16,7 +16,7 @@ // the first element is the string, the second one is the command. // - The output element is going to be a Vector of strings. -pub enum Command { +enum Command { Uppercase, Trim, Append(usize), diff --git a/exercises/quizzes/quiz3.rs b/exercises/quizzes/quiz3.rs index f255cb5d..f3cb1bcf 100644 --- a/exercises/quizzes/quiz3.rs +++ b/exercises/quizzes/quiz3.rs @@ -12,14 +12,14 @@ // to support alphabetical report cards. Change the Grade in the second test to // "A+" to show that your changes allow alphabetical grades. -pub struct ReportCard { - pub grade: f32, - pub student_name: String, - pub student_age: u8, +struct ReportCard { + grade: f32, + student_name: String, + student_age: u8, } impl ReportCard { - pub fn print(&self) -> String { + fn print(&self) -> String { format!( "{} ({}) - achieved a grade of {}", &self.student_name, &self.student_age, &self.grade From c8ad6c3960b4bec44a610cc144e6b635bffcbc31 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 22 May 2024 15:04:21 +0200 Subject: [PATCH 314/433] if1 solution --- rustlings-macros/info.toml | 4 ++-- solutions/03_if/if1.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 495e9c3e..a67e38d6 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -200,9 +200,9 @@ Some similar examples from other languages: - In Python this would be: `a if a > b else b` Remember in Rust that: -- the `if` condition does not need to be surrounded by parentheses +- The `if` condition does not need to be surrounded by parentheses - `if`/`else` conditionals are expressions -- Each condition is followed by a `{}` block.""" +- Each condition is followed by a `{}` block""" [[exercises]] name = "if2" diff --git a/solutions/03_if/if1.rs b/solutions/03_if/if1.rs index 4e181989..079c6715 100644 --- a/solutions/03_if/if1.rs +++ b/solutions/03_if/if1.rs @@ -1 +1,32 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn bigger(a: i32, b: i32) -> i32 { + if a > b { + a + } else { + b + } +} + +fn main() { + // You can optionally experiment here. +} + +// Don't mind this for now :) +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ten_is_bigger_than_eight() { + assert_eq!(10, bigger(10, 8)); + } + + #[test] + fn fortytwo_is_bigger_than_thirtytwo() { + assert_eq!(42, bigger(32, 42)); + } + + #[test] + fn equal_numbers() { + assert_eq!(42, bigger(42, 42)); + } +} From 7cdf6b79429428e944b440eb713e711d844a92d7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 22 May 2024 15:13:18 +0200 Subject: [PATCH 315/433] Add missing semicolons --- exercises/03_if/if2.rs | 6 +++--- exercises/03_if/if3.rs | 8 ++++---- exercises/04_primitive_types/primitive_types4.rs | 2 +- exercises/19_smart_pointers/box1.rs | 4 ++-- exercises/20_threads/threads3.rs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index d834ab27..1b65596b 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -20,16 +20,16 @@ mod tests { #[test] fn foo_for_fizz() { - assert_eq!(foo_if_fizz("fizz"), "foo") + assert_eq!(foo_if_fizz("fizz"), "foo"); } #[test] fn bar_for_fuzz() { - assert_eq!(foo_if_fizz("fuzz"), "bar") + assert_eq!(foo_if_fizz("fuzz"), "bar"); } #[test] fn default_to_baz() { - assert_eq!(foo_if_fizz("literally anything"), "baz") + assert_eq!(foo_if_fizz("literally anything"), "baz"); } } diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index ff6fee64..d3e4b06c 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -34,21 +34,21 @@ mod tests { #[test] fn gopher_lives_in_burrow() { - assert_eq!(animal_habitat("gopher"), "Burrow") + assert_eq!(animal_habitat("gopher"), "Burrow"); } #[test] fn snake_lives_in_desert() { - assert_eq!(animal_habitat("snake"), "Desert") + assert_eq!(animal_habitat("snake"), "Desert"); } #[test] fn crab_lives_on_beach() { - assert_eq!(animal_habitat("crab"), "Beach") + assert_eq!(animal_habitat("crab"), "Beach"); } #[test] fn unknown_animal() { - assert_eq!(animal_habitat("dinosaur"), "Unknown") + assert_eq!(animal_habitat("dinosaur"), "Unknown"); } } diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index c583ae13..661e0515 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -14,6 +14,6 @@ mod tests { let nice_slice = ??? - assert_eq!([2, 3, 4], nice_slice) + assert_eq!([2, 3, 4], nice_slice); } } diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index 908c9232..c8c2640d 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -42,11 +42,11 @@ mod tests { #[test] fn test_create_empty_list() { - assert_eq!(List::Nil, create_empty_list()) + assert_eq!(List::Nil, create_empty_list()); } #[test] fn test_create_non_empty_list() { - assert_ne!(create_empty_list(), create_non_empty_list()) + assert_ne!(create_empty_list(), create_non_empty_list()); } } diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index 13abc450..37810cf9 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -60,6 +60,6 @@ mod tests { } println!("total numbers received: {}", total_received); - assert_eq!(total_received, queue_length) + assert_eq!(total_received, queue_length); } } From eafb157d60ee46e95e2b54ec75b33180a838b0c5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 22 May 2024 15:16:50 +0200 Subject: [PATCH 316/433] if2 solution --- exercises/03_if/if2.rs | 8 ++++---- rustlings-macros/info.toml | 6 ++++-- solutions/03_if/if2.rs | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/exercises/03_if/if2.rs b/exercises/03_if/if2.rs index 1b65596b..593a77a7 100644 --- a/exercises/03_if/if2.rs +++ b/exercises/03_if/if2.rs @@ -1,6 +1,4 @@ -// Step 1: Make me compile! -// Step 2: Get the bar_for_fuzz and default_to_baz tests passing! - +// TODO: Fix the compiler error on this function. fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { "foo" @@ -13,13 +11,15 @@ fn main() { // You can optionally experiment here. } -// No test changes needed! +// TODO: Read the tests to understand the desired behavior. +// Make all tests pass without changing them. #[cfg(test)] mod tests { use super::*; #[test] fn foo_for_fizz() { + // This means that calling `foo_if_fizz` with the argument "fizz" should return "foo". assert_eq!(foo_if_fizz("fizz"), "foo"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index a67e38d6..39fc5c4d 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -209,8 +209,10 @@ name = "if2" dir = "03_if" hint = """ For that first compiler error, it's important in Rust that each conditional -block returns the same type! To get the tests passing, you will need a couple -conditions checking different input values.""" +block returns the same type! + +To get the tests passing, you will need a couple conditions checking different +input values. Read the tests to find out what they expect.""" [[exercises]] name = "if3" diff --git a/solutions/03_if/if2.rs b/solutions/03_if/if2.rs index 4e181989..440bba05 100644 --- a/solutions/03_if/if2.rs +++ b/solutions/03_if/if2.rs @@ -1 +1,33 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn foo_if_fizz(fizzish: &str) -> &str { + if fizzish == "fizz" { + "foo" + } else if fizzish == "fuzz" { + "bar" + } else { + "baz" + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn foo_for_fizz() { + assert_eq!(foo_if_fizz("fizz"), "foo"); + } + + #[test] + fn bar_for_fuzz() { + assert_eq!(foo_if_fizz("fuzz"), "bar"); + } + + #[test] + fn default_to_baz() { + assert_eq!(foo_if_fizz("literally anything"), "baz"); + } +} From 73e84f83791f00ef8ccfe438bc018d2c0a9b21fe Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 22 May 2024 15:54:35 +0200 Subject: [PATCH 317/433] if3 solution --- exercises/03_if/if3.rs | 21 ++++++++-------- solutions/03_if/if3.rs | 54 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/exercises/03_if/if3.rs b/exercises/03_if/if3.rs index d3e4b06c..89164eb2 100644 --- a/exercises/03_if/if3.rs +++ b/exercises/03_if/if3.rs @@ -1,4 +1,5 @@ -fn animal_habitat(animal: &str) -> &'static str { +fn animal_habitat(animal: &str) -> &str { + // TODO: Fix the compiler error in the statement below. let identifier = if animal == "crab" { 1 } else if animal == "gopher" { @@ -9,8 +10,8 @@ fn animal_habitat(animal: &str) -> &'static str { "Unknown" }; - // DO NOT CHANGE THIS STATEMENT BELOW - let habitat = if identifier == 1 { + // Don't change the expression below! + if identifier == 1 { "Beach" } else if identifier == 2 { "Burrow" @@ -18,37 +19,35 @@ fn animal_habitat(animal: &str) -> &'static str { "Desert" } else { "Unknown" - }; - - habitat + } } fn main() { // You can optionally experiment here. } -// No test changes needed. +// Don't change the tests! #[cfg(test)] mod tests { use super::*; #[test] fn gopher_lives_in_burrow() { - assert_eq!(animal_habitat("gopher"), "Burrow"); + assert_eq!(animal_habitat("gopher"), "Burrow") } #[test] fn snake_lives_in_desert() { - assert_eq!(animal_habitat("snake"), "Desert"); + assert_eq!(animal_habitat("snake"), "Desert") } #[test] fn crab_lives_on_beach() { - assert_eq!(animal_habitat("crab"), "Beach"); + assert_eq!(animal_habitat("crab"), "Beach") } #[test] fn unknown_animal() { - assert_eq!(animal_habitat("dinosaur"), "Unknown"); + assert_eq!(animal_habitat("dinosaur"), "Unknown") } } diff --git a/solutions/03_if/if3.rs b/solutions/03_if/if3.rs index 4e181989..571644d4 100644 --- a/solutions/03_if/if3.rs +++ b/solutions/03_if/if3.rs @@ -1 +1,53 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn animal_habitat(animal: &str) -> &str { + let identifier = if animal == "crab" { + 1 + } else if animal == "gopher" { + 2 + } else if animal == "snake" { + 3 + } else { + // Any unused identifier. + 4 + }; + + // Instead of such an identifier, you would use an enum in Rust. + // But we didn't get into enums yet. + if identifier == 1 { + "Beach" + } else if identifier == 2 { + "Burrow" + } else if identifier == 3 { + "Desert" + } else { + "Unknown" + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gopher_lives_in_burrow() { + assert_eq!(animal_habitat("gopher"), "Burrow") + } + + #[test] + fn snake_lives_in_desert() { + assert_eq!(animal_habitat("snake"), "Desert") + } + + #[test] + fn crab_lives_on_beach() { + assert_eq!(animal_habitat("crab"), "Beach") + } + + #[test] + fn unknown_animal() { + assert_eq!(animal_habitat("dinosaur"), "Unknown") + } +} From f2c3dcab3ac20e5aeddc7f792409727803da8bb8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 22 May 2024 16:35:57 +0200 Subject: [PATCH 318/433] quiz1 solution --- exercises/quizzes/quiz1.rs | 17 ++++++----------- solutions/quizzes/quiz1.rs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/exercises/quizzes/quiz1.rs b/exercises/quizzes/quiz1.rs index edb672ee..5f17514b 100644 --- a/exercises/quizzes/quiz1.rs +++ b/exercises/quizzes/quiz1.rs @@ -10,27 +10,22 @@ // quantity bought. // Put your function here! -// fn calculate_price_of_apples { +// fn calculate_price_of_apples(???) -> ??? { fn main() { // You can optionally experiment here. } +// Don't change the tests! #[cfg(test)] mod tests { use super::*; - // Don't modify this test! #[test] fn verify_test() { - let price1 = calculate_price_of_apples(35); - let price2 = calculate_price_of_apples(40); - let price3 = calculate_price_of_apples(41); - let price4 = calculate_price_of_apples(65); - - assert_eq!(70, price1); - assert_eq!(80, price2); - assert_eq!(41, price3); - assert_eq!(65, price4); + assert_eq!(calculate_price_of_apples(35), 70); + assert_eq!(calculate_price_of_apples(40), 80); + assert_eq!(calculate_price_of_apples(41), 41); + assert_eq!(calculate_price_of_apples(65), 65); } } diff --git a/solutions/quizzes/quiz1.rs b/solutions/quizzes/quiz1.rs index 4e181989..bc761667 100644 --- a/solutions/quizzes/quiz1.rs +++ b/solutions/quizzes/quiz1.rs @@ -1 +1,31 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Mary is buying apples. The price of an apple is calculated as follows: +// - An apple costs 2 rustbucks. +// - If Mary buys more than 40 apples, each apple only costs 1 rustbuck! +// Write a function that calculates the price of an order of apples given the +// quantity bought. + +fn calculate_price_of_apples(n_apples: u64) -> u64 { + if n_apples > 40 { + n_apples + } else { + 2 * n_apples + } +} + +fn main() { + // You can optionally experiment here. +} + +// Don't change the tests! +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn verify_test() { + assert_eq!(calculate_price_of_apples(35), 70); + assert_eq!(calculate_price_of_apples(40), 80); + assert_eq!(calculate_price_of_apples(41), 41); + assert_eq!(calculate_price_of_apples(65), 65); + } +} From 8d4145038d8b1a2a31314669cc17dc42b1c8e616 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 25 May 2024 16:15:35 +0200 Subject: [PATCH 319/433] Update deps --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10dfa306..e1044ff0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -825,9 +825,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.65" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", From 990c68efcba8e1e2b7f2d8c5b6c16885d3920010 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 25 May 2024 16:31:21 +0200 Subject: [PATCH 320/433] primitive_types1 solution --- exercises/04_primitive_types/primitive_types1.rs | 8 +++----- rustlings-macros/info.toml | 5 ++++- solutions/04_primitive_types/primitive_types1.rs | 12 +++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs index 0002651d..750d6e55 100644 --- a/exercises/04_primitive_types/primitive_types1.rs +++ b/exercises/04_primitive_types/primitive_types1.rs @@ -1,14 +1,12 @@ -// Fill in the rest of the line that has code missing! - fn main() { - // Booleans (`bool`) - let is_morning = true; if is_morning { println!("Good morning!"); } - let // Finish the rest of this line like the example! Or make it be false! + // TODO: Define a boolean variable with the name `is_evening` before the `if` statement below. + // The value of the variable should be the negation (opposite) of `is_morning`. + // let … if is_evening { println!("Good evening!"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 39fc5c4d..59de7f25 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -234,7 +234,10 @@ hint = "No hints this time ;)" name = "primitive_types1" dir = "04_primitive_types" test = false -hint = "No hints this time ;)" +hint = """ +In Rust, a boolean can be negated using the operator `!` before it. +Example: `!true == false` +This also works with boolean variables.""" [[exercises]] name = "primitive_types2" diff --git a/solutions/04_primitive_types/primitive_types1.rs b/solutions/04_primitive_types/primitive_types1.rs index 4e181989..fac6ec04 100644 --- a/solutions/04_primitive_types/primitive_types1.rs +++ b/solutions/04_primitive_types/primitive_types1.rs @@ -1 +1,11 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + let is_morning = true; + if is_morning { + println!("Good morning!"); + } + + let is_evening = !is_morning; + if is_evening { + println!("Good evening!"); + } +} From beb7b24e8e9fe05cbcfaaf4676d52f63351fad16 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 25 May 2024 18:19:30 +0200 Subject: [PATCH 321/433] Add solutions to bins --- solutions/00_intro/intro1.rs | 7 +++++-- src/cargo_toml.rs | 19 +++++++++++++++++++ src/embedded.rs | 18 +++++++----------- src/info_file.rs | 24 ++++++++++++++++++++++++ src/init.rs | 20 ++++++++++++++++++++ 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/solutions/00_intro/intro1.rs b/solutions/00_intro/intro1.rs index 07d4e4fd..4fe84549 100644 --- a/solutions/00_intro/intro1.rs +++ b/solutions/00_intro/intro1.rs @@ -1,2 +1,5 @@ -// The exercise `intro1` only requires entering `n` in the terminal to go to the next exercise. -// It is just an introduction to how Rustlings works. +fn main() { + // Congratulations, you finished the first exercise 🎉 + // As an introduction to Rustlings, the first exercise only required + // entering `n` in the terminal to go to the next exercise. +} diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index b7951f6b..cf17acd4 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use std::path::Path; use crate::info_file::ExerciseInfo; @@ -40,6 +41,24 @@ pub fn append_bins( } buf.extend_from_slice(exercise_info.name.as_bytes()); buf.extend_from_slice(b".rs\" },\n"); + + let sol_path = exercise_info.sol_path(); + if !Path::new(&sol_path).exists() { + continue; + } + + buf.extend_from_slice(b" { name = \""); + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b"_sol"); + buf.extend_from_slice(b"\", path = \""); + buf.extend_from_slice(exercise_path_prefix); + buf.extend_from_slice(b"solutions/"); + if let Some(dir) = &exercise_info.dir { + buf.extend_from_slice(dir.as_bytes()); + buf.push(b'/'); + } + buf.extend_from_slice(exercise_info.name.as_bytes()); + buf.extend_from_slice(b".rs\" },\n"); } } diff --git a/src/embedded.rs b/src/embedded.rs index e710a4e5..1dce46c5 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error, Result}; use std::{ - fs::{create_dir, create_dir_all, OpenOptions}, + fs::{create_dir, OpenOptions}, io::{self, Write}, }; @@ -43,8 +43,8 @@ struct ExerciseFiles { } // A directory in the `exercises/` directory. -struct ExerciseDir { - name: &'static str, +pub struct ExerciseDir { + pub name: &'static str, readme: &'static [u8], } @@ -78,7 +78,7 @@ pub struct EmbeddedFiles { /// The content of the `info.toml` file. pub info_file: &'static str, exercise_files: &'static [ExerciseFiles], - exercise_dirs: &'static [ExerciseDir], + pub exercise_dirs: &'static [ExerciseDir], } impl EmbeddedFiles { @@ -121,13 +121,9 @@ impl EmbeddedFiles { // 14 = 10 + 1 + 3 // solutions/ + / + .rs - let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); - dir_path.push_str("solutions/"); - dir_path.push_str(dir.name); - create_dir_all(&dir_path) - .with_context(|| format!("Failed to create the directory {dir_path}"))?; - - let mut solution_path = dir_path; + let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); + solution_path.push_str("solutions/"); + solution_path.push_str(dir.name); solution_path.push('/'); solution_path.push_str(exercise_name); solution_path.push_str(".rs"); diff --git a/src/info_file.rs b/src/info_file.rs index 0c459284..5ea487fc 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -49,6 +49,30 @@ impl ExerciseInfo { path } + + /// Path to the solution file starting with the `solutions/` directory. + pub fn sol_path(&self) -> String { + let mut path = if let Some(dir) = &self.dir { + // 14 = 10 + 1 + 3 + // solutions/ + / + .rs + let mut path = String::with_capacity(14 + dir.len() + self.name.len()); + path.push_str("solutions/"); + path.push_str(dir); + path.push('/'); + path + } else { + // 13 = 10 + 3 + // solutions/ + .rs + let mut path = String::with_capacity(13 + self.name.len()); + path.push_str("solutions/"); + path + }; + + path.push_str(&self.name); + path.push_str(".rs"); + + path + } } /// The deserialized `info.toml` file. diff --git a/src/init.rs b/src/init.rs index 67d8a243..4063ca75 100644 --- a/src/init.rs +++ b/src/init.rs @@ -34,6 +34,20 @@ pub fn init() -> Result<()> { .init_exercises_dir(&info_file.exercises) .context("Failed to initialize the `rustlings/exercises` directory")?; + create_dir("solutions").context("Failed to create the `solutions/` directory")?; + for dir in EMBEDDED_FILES.exercise_dirs { + let mut dir_path = String::with_capacity(10 + dir.name.len()); + dir_path.push_str("solutions/"); + dir_path.push_str(dir.name); + create_dir(&dir_path) + .with_context(|| format!("Failed to create the directory {dir_path}"))?; + } + for exercise_info in &info_file.exercises { + let solution_path = exercise_info.sol_path(); + fs::write(&solution_path, INIT_SOLUTION_FILE) + .with_context(|| format!("Failed to create the file {solution_path}"))?; + } + let current_cargo_toml = include_str!("../dev-Cargo.toml"); // Skip the first line (comment). let newline_ind = current_cargo_toml @@ -72,6 +86,12 @@ pub fn init() -> Result<()> { Ok(()) } +const INIT_SOLUTION_FILE: &[u8] = b"fn main() { + // DON'T EDIT THIS SOLUTION FILE! + // It will be automatically filled after you finish the exercise. +} +"; + const GITIGNORE: &[u8] = b".rustlings-state.txt solutions Cargo.lock From 84a818dbda156aeccf97d8dc0a33daeef084013a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 15:01:18 +0200 Subject: [PATCH 322/433] Update the bins buffer capacity --- src/cargo_toml.rs | 5 ++++- src/dev/check.rs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index cf17acd4..c4d6700a 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -3,6 +3,9 @@ use std::path::Path; use crate::info_file::ExerciseInfo; +/// Initial capacity of the bins buffer. +pub const BINS_BUFFER_CAPACITY: usize = 1 << 14; + /// Return the start and end index of the content of the list `bin = […]`. /// bin = [xxxxxxxxxxxxxxxxx] /// |start_ind | @@ -70,7 +73,7 @@ pub fn updated_cargo_toml( ) -> Result> { let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; - let mut updated_cargo_toml = Vec::with_capacity(1 << 13); + let mut updated_cargo_toml = Vec::with_capacity(BINS_BUFFER_CAPACITY); updated_cargo_toml.extend_from_slice(current_cargo_toml[..bins_start_ind].as_bytes()); append_bins( &mut updated_cargo_toml, diff --git a/src/dev/check.rs b/src/dev/check.rs index 81d05ce6..7c35b4f3 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -7,7 +7,7 @@ use std::{ }; use crate::{ - cargo_toml::{append_bins, bins_start_end_ind}, + cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, DEBUG_PROFILE, }; @@ -145,7 +145,7 @@ fn check_cargo_toml( let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; - let mut new_bins = Vec::with_capacity(1 << 13); + let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); if old_bins != new_bins { From 1984a8d38eddb037498afe4fc42351253ff58b05 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 15:01:27 +0200 Subject: [PATCH 323/433] Update Cargo.toml with the solution bins --- dev/Cargo.toml | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index ad39debf..3283871a 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -1,101 +1,197 @@ # Don't edit the `bin` list manually! It is updated by `cargo run -- dev update`. This comment line will be stripped in `rustlings init`. bin = [ { name = "intro1", path = "../exercises/00_intro/intro1.rs" }, + { name = "intro1_sol", path = "../solutions/00_intro/intro1.rs" }, { name = "intro2", path = "../exercises/00_intro/intro2.rs" }, + { name = "intro2_sol", path = "../solutions/00_intro/intro2.rs" }, { name = "variables1", path = "../exercises/01_variables/variables1.rs" }, + { name = "variables1_sol", path = "../solutions/01_variables/variables1.rs" }, { name = "variables2", path = "../exercises/01_variables/variables2.rs" }, + { name = "variables2_sol", path = "../solutions/01_variables/variables2.rs" }, { name = "variables3", path = "../exercises/01_variables/variables3.rs" }, + { name = "variables3_sol", path = "../solutions/01_variables/variables3.rs" }, { name = "variables4", path = "../exercises/01_variables/variables4.rs" }, + { name = "variables4_sol", path = "../solutions/01_variables/variables4.rs" }, { name = "variables5", path = "../exercises/01_variables/variables5.rs" }, + { name = "variables5_sol", path = "../solutions/01_variables/variables5.rs" }, { name = "variables6", path = "../exercises/01_variables/variables6.rs" }, + { name = "variables6_sol", path = "../solutions/01_variables/variables6.rs" }, { name = "functions1", path = "../exercises/02_functions/functions1.rs" }, + { name = "functions1_sol", path = "../solutions/02_functions/functions1.rs" }, { name = "functions2", path = "../exercises/02_functions/functions2.rs" }, + { name = "functions2_sol", path = "../solutions/02_functions/functions2.rs" }, { name = "functions3", path = "../exercises/02_functions/functions3.rs" }, + { name = "functions3_sol", path = "../solutions/02_functions/functions3.rs" }, { name = "functions4", path = "../exercises/02_functions/functions4.rs" }, + { name = "functions4_sol", path = "../solutions/02_functions/functions4.rs" }, { name = "functions5", path = "../exercises/02_functions/functions5.rs" }, + { name = "functions5_sol", path = "../solutions/02_functions/functions5.rs" }, { name = "if1", path = "../exercises/03_if/if1.rs" }, + { name = "if1_sol", path = "../solutions/03_if/if1.rs" }, { name = "if2", path = "../exercises/03_if/if2.rs" }, + { name = "if2_sol", path = "../solutions/03_if/if2.rs" }, { name = "if3", path = "../exercises/03_if/if3.rs" }, + { name = "if3_sol", path = "../solutions/03_if/if3.rs" }, { name = "quiz1", path = "../exercises/quizzes/quiz1.rs" }, + { name = "quiz1_sol", path = "../solutions/quizzes/quiz1.rs" }, { name = "primitive_types1", path = "../exercises/04_primitive_types/primitive_types1.rs" }, + { name = "primitive_types1_sol", path = "../solutions/04_primitive_types/primitive_types1.rs" }, { name = "primitive_types2", path = "../exercises/04_primitive_types/primitive_types2.rs" }, + { name = "primitive_types2_sol", path = "../solutions/04_primitive_types/primitive_types2.rs" }, { name = "primitive_types3", path = "../exercises/04_primitive_types/primitive_types3.rs" }, + { name = "primitive_types3_sol", path = "../solutions/04_primitive_types/primitive_types3.rs" }, { name = "primitive_types4", path = "../exercises/04_primitive_types/primitive_types4.rs" }, + { name = "primitive_types4_sol", path = "../solutions/04_primitive_types/primitive_types4.rs" }, { name = "primitive_types5", path = "../exercises/04_primitive_types/primitive_types5.rs" }, + { name = "primitive_types5_sol", path = "../solutions/04_primitive_types/primitive_types5.rs" }, { name = "primitive_types6", path = "../exercises/04_primitive_types/primitive_types6.rs" }, + { name = "primitive_types6_sol", path = "../solutions/04_primitive_types/primitive_types6.rs" }, { name = "vecs1", path = "../exercises/05_vecs/vecs1.rs" }, + { name = "vecs1_sol", path = "../solutions/05_vecs/vecs1.rs" }, { name = "vecs2", path = "../exercises/05_vecs/vecs2.rs" }, + { name = "vecs2_sol", path = "../solutions/05_vecs/vecs2.rs" }, { name = "move_semantics1", path = "../exercises/06_move_semantics/move_semantics1.rs" }, + { name = "move_semantics1_sol", path = "../solutions/06_move_semantics/move_semantics1.rs" }, { name = "move_semantics2", path = "../exercises/06_move_semantics/move_semantics2.rs" }, + { name = "move_semantics2_sol", path = "../solutions/06_move_semantics/move_semantics2.rs" }, { name = "move_semantics3", path = "../exercises/06_move_semantics/move_semantics3.rs" }, + { name = "move_semantics3_sol", path = "../solutions/06_move_semantics/move_semantics3.rs" }, { name = "move_semantics4", path = "../exercises/06_move_semantics/move_semantics4.rs" }, + { name = "move_semantics4_sol", path = "../solutions/06_move_semantics/move_semantics4.rs" }, { name = "move_semantics5", path = "../exercises/06_move_semantics/move_semantics5.rs" }, + { name = "move_semantics5_sol", path = "../solutions/06_move_semantics/move_semantics5.rs" }, { name = "move_semantics6", path = "../exercises/06_move_semantics/move_semantics6.rs" }, + { name = "move_semantics6_sol", path = "../solutions/06_move_semantics/move_semantics6.rs" }, { name = "structs1", path = "../exercises/07_structs/structs1.rs" }, + { name = "structs1_sol", path = "../solutions/07_structs/structs1.rs" }, { name = "structs2", path = "../exercises/07_structs/structs2.rs" }, + { name = "structs2_sol", path = "../solutions/07_structs/structs2.rs" }, { name = "structs3", path = "../exercises/07_structs/structs3.rs" }, + { name = "structs3_sol", path = "../solutions/07_structs/structs3.rs" }, { name = "enums1", path = "../exercises/08_enums/enums1.rs" }, + { name = "enums1_sol", path = "../solutions/08_enums/enums1.rs" }, { name = "enums2", path = "../exercises/08_enums/enums2.rs" }, + { name = "enums2_sol", path = "../solutions/08_enums/enums2.rs" }, { name = "enums3", path = "../exercises/08_enums/enums3.rs" }, + { name = "enums3_sol", path = "../solutions/08_enums/enums3.rs" }, { name = "strings1", path = "../exercises/09_strings/strings1.rs" }, + { name = "strings1_sol", path = "../solutions/09_strings/strings1.rs" }, { name = "strings2", path = "../exercises/09_strings/strings2.rs" }, + { name = "strings2_sol", path = "../solutions/09_strings/strings2.rs" }, { name = "strings3", path = "../exercises/09_strings/strings3.rs" }, + { name = "strings3_sol", path = "../solutions/09_strings/strings3.rs" }, { name = "strings4", path = "../exercises/09_strings/strings4.rs" }, + { name = "strings4_sol", path = "../solutions/09_strings/strings4.rs" }, { name = "modules1", path = "../exercises/10_modules/modules1.rs" }, + { name = "modules1_sol", path = "../solutions/10_modules/modules1.rs" }, { name = "modules2", path = "../exercises/10_modules/modules2.rs" }, + { name = "modules2_sol", path = "../solutions/10_modules/modules2.rs" }, { name = "modules3", path = "../exercises/10_modules/modules3.rs" }, + { name = "modules3_sol", path = "../solutions/10_modules/modules3.rs" }, { name = "hashmaps1", path = "../exercises/11_hashmaps/hashmaps1.rs" }, + { name = "hashmaps1_sol", path = "../solutions/11_hashmaps/hashmaps1.rs" }, { name = "hashmaps2", path = "../exercises/11_hashmaps/hashmaps2.rs" }, + { name = "hashmaps2_sol", path = "../solutions/11_hashmaps/hashmaps2.rs" }, { name = "hashmaps3", path = "../exercises/11_hashmaps/hashmaps3.rs" }, + { name = "hashmaps3_sol", path = "../solutions/11_hashmaps/hashmaps3.rs" }, { name = "quiz2", path = "../exercises/quizzes/quiz2.rs" }, + { name = "quiz2_sol", path = "../solutions/quizzes/quiz2.rs" }, { name = "options1", path = "../exercises/12_options/options1.rs" }, + { name = "options1_sol", path = "../solutions/12_options/options1.rs" }, { name = "options2", path = "../exercises/12_options/options2.rs" }, + { name = "options2_sol", path = "../solutions/12_options/options2.rs" }, { name = "options3", path = "../exercises/12_options/options3.rs" }, + { name = "options3_sol", path = "../solutions/12_options/options3.rs" }, { name = "errors1", path = "../exercises/13_error_handling/errors1.rs" }, + { name = "errors1_sol", path = "../solutions/13_error_handling/errors1.rs" }, { name = "errors2", path = "../exercises/13_error_handling/errors2.rs" }, + { name = "errors2_sol", path = "../solutions/13_error_handling/errors2.rs" }, { name = "errors3", path = "../exercises/13_error_handling/errors3.rs" }, + { name = "errors3_sol", path = "../solutions/13_error_handling/errors3.rs" }, { name = "errors4", path = "../exercises/13_error_handling/errors4.rs" }, + { name = "errors4_sol", path = "../solutions/13_error_handling/errors4.rs" }, { name = "errors5", path = "../exercises/13_error_handling/errors5.rs" }, + { name = "errors5_sol", path = "../solutions/13_error_handling/errors5.rs" }, { name = "errors6", path = "../exercises/13_error_handling/errors6.rs" }, + { name = "errors6_sol", path = "../solutions/13_error_handling/errors6.rs" }, { name = "generics1", path = "../exercises/14_generics/generics1.rs" }, + { name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" }, { name = "generics2", path = "../exercises/14_generics/generics2.rs" }, + { name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" }, { name = "traits1", path = "../exercises/15_traits/traits1.rs" }, + { name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" }, { name = "traits2", path = "../exercises/15_traits/traits2.rs" }, + { name = "traits2_sol", path = "../solutions/15_traits/traits2.rs" }, { name = "traits3", path = "../exercises/15_traits/traits3.rs" }, + { name = "traits3_sol", path = "../solutions/15_traits/traits3.rs" }, { name = "traits4", path = "../exercises/15_traits/traits4.rs" }, + { name = "traits4_sol", path = "../solutions/15_traits/traits4.rs" }, { name = "traits5", path = "../exercises/15_traits/traits5.rs" }, + { name = "traits5_sol", path = "../solutions/15_traits/traits5.rs" }, { name = "quiz3", path = "../exercises/quizzes/quiz3.rs" }, + { name = "quiz3_sol", path = "../solutions/quizzes/quiz3.rs" }, { name = "lifetimes1", path = "../exercises/16_lifetimes/lifetimes1.rs" }, + { name = "lifetimes1_sol", path = "../solutions/16_lifetimes/lifetimes1.rs" }, { name = "lifetimes2", path = "../exercises/16_lifetimes/lifetimes2.rs" }, + { name = "lifetimes2_sol", path = "../solutions/16_lifetimes/lifetimes2.rs" }, { name = "lifetimes3", path = "../exercises/16_lifetimes/lifetimes3.rs" }, + { name = "lifetimes3_sol", path = "../solutions/16_lifetimes/lifetimes3.rs" }, { name = "tests1", path = "../exercises/17_tests/tests1.rs" }, + { name = "tests1_sol", path = "../solutions/17_tests/tests1.rs" }, { name = "tests2", path = "../exercises/17_tests/tests2.rs" }, + { name = "tests2_sol", path = "../solutions/17_tests/tests2.rs" }, { name = "tests3", path = "../exercises/17_tests/tests3.rs" }, + { name = "tests3_sol", path = "../solutions/17_tests/tests3.rs" }, { name = "tests4", path = "../exercises/17_tests/tests4.rs" }, + { name = "tests4_sol", path = "../solutions/17_tests/tests4.rs" }, { name = "iterators1", path = "../exercises/18_iterators/iterators1.rs" }, + { name = "iterators1_sol", path = "../solutions/18_iterators/iterators1.rs" }, { name = "iterators2", path = "../exercises/18_iterators/iterators2.rs" }, + { name = "iterators2_sol", path = "../solutions/18_iterators/iterators2.rs" }, { name = "iterators3", path = "../exercises/18_iterators/iterators3.rs" }, + { name = "iterators3_sol", path = "../solutions/18_iterators/iterators3.rs" }, { name = "iterators4", path = "../exercises/18_iterators/iterators4.rs" }, + { name = "iterators4_sol", path = "../solutions/18_iterators/iterators4.rs" }, { name = "iterators5", path = "../exercises/18_iterators/iterators5.rs" }, + { name = "iterators5_sol", path = "../solutions/18_iterators/iterators5.rs" }, { name = "box1", path = "../exercises/19_smart_pointers/box1.rs" }, + { name = "box1_sol", path = "../solutions/19_smart_pointers/box1.rs" }, { name = "rc1", path = "../exercises/19_smart_pointers/rc1.rs" }, + { name = "rc1_sol", path = "../solutions/19_smart_pointers/rc1.rs" }, { name = "arc1", path = "../exercises/19_smart_pointers/arc1.rs" }, + { name = "arc1_sol", path = "../solutions/19_smart_pointers/arc1.rs" }, { name = "cow1", path = "../exercises/19_smart_pointers/cow1.rs" }, + { name = "cow1_sol", path = "../solutions/19_smart_pointers/cow1.rs" }, { name = "threads1", path = "../exercises/20_threads/threads1.rs" }, + { name = "threads1_sol", path = "../solutions/20_threads/threads1.rs" }, { name = "threads2", path = "../exercises/20_threads/threads2.rs" }, + { name = "threads2_sol", path = "../solutions/20_threads/threads2.rs" }, { name = "threads3", path = "../exercises/20_threads/threads3.rs" }, + { name = "threads3_sol", path = "../solutions/20_threads/threads3.rs" }, { name = "macros1", path = "../exercises/21_macros/macros1.rs" }, + { name = "macros1_sol", path = "../solutions/21_macros/macros1.rs" }, { name = "macros2", path = "../exercises/21_macros/macros2.rs" }, + { name = "macros2_sol", path = "../solutions/21_macros/macros2.rs" }, { name = "macros3", path = "../exercises/21_macros/macros3.rs" }, + { name = "macros3_sol", path = "../solutions/21_macros/macros3.rs" }, { name = "macros4", path = "../exercises/21_macros/macros4.rs" }, + { name = "macros4_sol", path = "../solutions/21_macros/macros4.rs" }, { name = "clippy1", path = "../exercises/22_clippy/clippy1.rs" }, + { name = "clippy1_sol", path = "../solutions/22_clippy/clippy1.rs" }, { name = "clippy2", path = "../exercises/22_clippy/clippy2.rs" }, + { name = "clippy2_sol", path = "../solutions/22_clippy/clippy2.rs" }, { name = "clippy3", path = "../exercises/22_clippy/clippy3.rs" }, + { name = "clippy3_sol", path = "../solutions/22_clippy/clippy3.rs" }, { name = "using_as", path = "../exercises/23_conversions/using_as.rs" }, + { name = "using_as_sol", path = "../solutions/23_conversions/using_as.rs" }, { name = "from_into", path = "../exercises/23_conversions/from_into.rs" }, + { name = "from_into_sol", path = "../solutions/23_conversions/from_into.rs" }, { name = "from_str", path = "../exercises/23_conversions/from_str.rs" }, + { name = "from_str_sol", path = "../solutions/23_conversions/from_str.rs" }, { name = "try_from_into", path = "../exercises/23_conversions/try_from_into.rs" }, + { name = "try_from_into_sol", path = "../solutions/23_conversions/try_from_into.rs" }, { name = "as_ref_mut", path = "../exercises/23_conversions/as_ref_mut.rs" }, + { name = "as_ref_mut_sol", path = "../solutions/23_conversions/as_ref_mut.rs" }, ] [package] From c324ea10df1e8a19cd9fae9be0eda28d99b9f2e5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 15:10:17 +0200 Subject: [PATCH 324/433] Update deps --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1044ff0..fec0365a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,9 +565,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" dependencies = [ "unicode-ident", ] @@ -705,18 +705,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1099,9 +1099,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" +checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index ebcbe6cf..90e329eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ license = "MIT" edition = "2021" [workspace.dependencies] -serde = { version = "1.0.202", features = ["derive"] } +serde = { version = "1.0.203", features = ["derive"] } toml_edit = { version = "0.22.13", default-features = false, features = ["parse", "serde"] } [package] From 8e9c99ae5bfd9212c7d2a2c78186ab82133e69c7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 15:10:43 +0200 Subject: [PATCH 325/433] Change condition order --- src/dev/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 7c35b4f3..59352e96 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -14,7 +14,7 @@ use crate::{ // Find a char that isn't allowed in the exercise's `name` or `dir`. fn forbidden_char(input: &str) -> Option { - input.chars().find(|c| *c != '_' && !c.is_alphanumeric()) + input.chars().find(|c| !c.is_alphanumeric() && *c != '_') } // Check the info of all exercises and return their paths in a set. From 611f9d8722593430d82187aebee9db5cc6952da1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 21:48:15 +0200 Subject: [PATCH 326/433] Check that all solutions run successfully --- src/app_state.rs | 41 ++++++++------- src/cmd.rs | 4 +- src/dev/check.rs | 57 ++++++++++++++------ src/exercise.rs | 126 +++++++++++++++++++++++++++++---------------- src/info_file.rs | 19 ++++++- src/run.rs | 4 +- src/watch/state.rs | 4 +- 7 files changed, 169 insertions(+), 86 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index c7c090f6..e9a5b109 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -11,7 +11,7 @@ use std::{ use crate::{ clear_terminal, embedded::EMBEDDED_FILES, - exercise::{Exercise, OUTPUT_CAPACITY}, + exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY}, info_file::ExerciseInfo, DEBUG_PROFILE, }; @@ -40,6 +40,25 @@ struct CargoMetadata { target_directory: PathBuf, } +pub fn parse_target_dir() -> Result { + // Get the target directory from Cargo. + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + + serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output") + .map(|metadata| metadata.target_directory) +} + pub struct AppState { current_exercise_ind: usize, exercises: Vec, @@ -104,23 +123,7 @@ impl AppState { exercise_infos: Vec, final_message: String, ) -> Result<(Self, StateFileStatus)> { - // Get the target directory from Cargo. - let metadata_output = Command::new("cargo") - .arg("metadata") - .arg("-q") - .arg("--format-version") - .arg("1") - .arg("--no-deps") - .stdin(Stdio::null()) - .stderr(Stdio::inherit()) - .output() - .context(CARGO_METADATA_ERR)? - .stdout; - let target_dir = serde_json::de::from_slice::(&metadata_output) - .context( - "Failed to read the field `target_directory` from the `cargo metadata` output", - )? - .target_directory; + let target_dir = parse_target_dir()?; let exercises = exercise_infos .into_iter() @@ -381,7 +384,7 @@ impl AppState { write!(writer, "Running {exercise} ... ")?; writer.flush()?; - let success = exercise.run(&mut output, &self.target_dir)?; + let success = exercise.run_exercise(&mut output, &self.target_dir)?; if !success { writeln!(writer, "{}\n", "FAILED".red())?; diff --git a/src/cmd.rs b/src/cmd.rs index b914ed88..6092f531 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -35,7 +35,7 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Res pub struct CargoCmd<'a> { pub subcommand: &'a str, pub args: &'a [&'a str], - pub exercise_name: &'a str, + pub bin_name: &'a str, pub description: &'a str, /// RUSTFLAGS="-A warnings" pub hide_warnings: bool, @@ -65,7 +65,7 @@ impl<'a> CargoCmd<'a> { .arg("always") .arg("-q") .arg("--bin") - .arg(self.exercise_name) + .arg(self.bin_name) .args(self.args); if self.hide_warnings { diff --git a/src/dev/check.rs b/src/dev/check.rs index 59352e96..6a3597cc 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -2,12 +2,14 @@ use anyhow::{anyhow, bail, Context, Error, Result}; use std::{ cmp::Ordering, fs::{self, read_dir, OpenOptions}, - io::Read, + io::{self, Read, Write}, path::{Path, PathBuf}, }; use crate::{ + app_state::parse_target_dir, cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY}, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, info_file::{ExerciseInfo, InfoFile}, CURRENT_FORMAT_VERSION, DEBUG_PROFILE, }; @@ -17,6 +19,29 @@ fn forbidden_char(input: &str) -> Option { input.chars().find(|c| !c.is_alphanumeric() && *c != '_') } +// Check that the Cargo.toml file is up-to-date. +fn check_cargo_toml( + exercise_infos: &[ExerciseInfo], + current_cargo_toml: &str, + exercise_path_prefix: &[u8], +) -> Result<()> { + let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; + + let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; + let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); + append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); + + if old_bins != new_bins { + if DEBUG_PROFILE { + bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + } + + bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); + } + + Ok(()) +} + // Check the info of all exercises and return their paths in a set. fn check_info_file_exercises(info_file: &InfoFile) -> Result> { let mut names = hashbrown::HashSet::with_capacity(info_file.exercises.len()); @@ -136,24 +161,20 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { Ok(()) } -// Check that the Cargo.toml file is up-to-date. -fn check_cargo_toml( - exercise_infos: &[ExerciseInfo], - current_cargo_toml: &str, - exercise_path_prefix: &[u8], -) -> Result<()> { - let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?; +fn check_solutions(info_file: &InfoFile) -> Result<()> { + let target_dir = parse_target_dir()?; + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind]; - let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY); - append_bins(&mut new_bins, exercise_infos, exercise_path_prefix); + for exercise_info in &info_file.exercises { + let success = exercise_info.run_solution(&mut output, &target_dir)?; + if !success { + io::stderr().write_all(&output)?; - if old_bins != new_bins { - if DEBUG_PROFILE { - bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); + bail!( + "Failed to run the solution of the exercise {}", + exercise_info.name, + ); } - - bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); } Ok(()) @@ -161,7 +182,6 @@ fn check_cargo_toml( pub fn check() -> Result<()> { let info_file = InfoFile::parse()?; - check_exercises(&info_file)?; // A hack to make `cargo run -- dev check` work when developing Rustlings. if DEBUG_PROFILE { @@ -176,6 +196,9 @@ pub fn check() -> Result<()> { check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?; } + check_exercises(&info_file)?; + check_solutions(&info_file)?; + println!("\nEverything looks fine!"); Ok(()) diff --git a/src/exercise.rs b/src/exercise.rs index 4bc37cd7..b6adc141 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -17,6 +17,35 @@ use crate::{ /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; +// Run an exercise binary and append its output to the `output` buffer. +// Compilation must be done before calling this method. +fn run_bin(bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { + writeln!(output, "{}", "Output".underlined())?; + + // 7 = "/debug/".len() + let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len()); + bin_path.push(target_dir); + bin_path.push("debug"); + bin_path.push(bin_name); + + let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; + + if !success { + // This output is important to show the user that something went wrong. + // Otherwise, calling something like `exit(1)` in an exercise without further output + // leaves the user confused about why the exercise isn't done yet. + writeln!( + output, + "{}", + "The exercise didn't run successfully (nonzero exit code)" + .bold() + .red(), + )?; + } + + Ok(success) +} + /// See `info_file::ExerciseInfo` pub struct Exercise { pub dir: Option<&'static str>, @@ -30,39 +59,25 @@ pub struct Exercise { } impl Exercise { - // Run the exercise's binary and append its output to the `output` buffer. - // Compilation should be done before calling this method. - fn run_bin(&self, output: &mut Vec, target_dir: &Path) -> Result { - writeln!(output, "{}", "Output".underlined())?; - - // 7 = "/debug/".len() - let mut bin_path = - PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); - bin_path.push(target_dir); - bin_path.push("debug"); - bin_path.push(self.name); - - let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; - - if !success { - // This output is important to show the user that something went wrong. - // Otherwise, calling something like `exit(1)` in an exercise without further output - // leaves the user confused about why the exercise isn't done yet. - writeln!( - output, - "{}", - "The exercise didn't run successfully (nonzero exit code)" - .bold() - .red(), - )?; - } - - Ok(success) + pub fn terminal_link(&self) -> StyledContent> { + style(TerminalFileLink(self.path)).underlined().blue() } +} - /// Compile, check and run the exercise. - /// The output is written to the `output` buffer after clearing it. - pub fn run(&self, output: &mut Vec, target_dir: &Path) -> Result { +impl Display for Exercise { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.path.fmt(f) + } +} + +pub trait RunnableExercise { + fn name(&self) -> &str; + fn strict_clippy(&self) -> bool; + fn test(&self) -> bool; + + // Compile, check and run the exercise or its solution (depending on `bin_name´). + // The output is written to the `output` buffer after clearing it. + fn run(&self, bin_name: &str, output: &mut Vec, target_dir: &Path) -> Result { output.clear(); // Developing the official Rustlings. @@ -71,7 +86,7 @@ impl Exercise { let build_success = CargoCmd { subcommand: "build", args: &[], - exercise_name: self.name, + bin_name, description: "cargo build …", hide_warnings: false, target_dir, @@ -87,7 +102,7 @@ impl Exercise { output.clear(); // `--profile test` is required to also check code with `[cfg(test)]`. - let clippy_args: &[&str] = if self.strict_clippy { + let clippy_args: &[&str] = if self.strict_clippy() { &["--profile", "test", "--", "-D", "warnings"] } else { &["--profile", "test"] @@ -95,7 +110,7 @@ impl Exercise { let clippy_success = CargoCmd { subcommand: "clippy", args: clippy_args, - exercise_name: self.name, + bin_name, description: "cargo clippy …", hide_warnings: false, target_dir, @@ -107,14 +122,14 @@ impl Exercise { return Ok(false); } - if !self.test { - return self.run_bin(output, target_dir); + if !self.test() { + return run_bin(bin_name, output, target_dir); } let test_success = CargoCmd { subcommand: "test", args: &["--", "--color", "always", "--show-output"], - exercise_name: self.name, + bin_name, description: "cargo test …", // Hide warnings because they are shown by Clippy. hide_warnings: true, @@ -124,18 +139,43 @@ impl Exercise { } .run()?; - let run_success = self.run_bin(output, target_dir)?; + let run_success = run_bin(bin_name, output, target_dir)?; Ok(test_success && run_success) } - pub fn terminal_link(&self) -> StyledContent> { - style(TerminalFileLink(self.path)).underlined().blue() + /// Compile, check and run the exercise. + /// The output is written to the `output` buffer after clearing it. + #[inline] + fn run_exercise(&self, output: &mut Vec, target_dir: &Path) -> Result { + self.run(self.name(), output, target_dir) + } + + /// Compile, check and run the exercise's solution. + /// The output is written to the `output` buffer after clearing it. + fn run_solution(&self, output: &mut Vec, target_dir: &Path) -> Result { + let name = self.name(); + let mut bin_name = String::with_capacity(name.len()); + bin_name.push_str(name); + bin_name.push_str("_sol"); + + self.run(&bin_name, output, target_dir) } } -impl Display for Exercise { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.path.fmt(f) +impl RunnableExercise for Exercise { + #[inline] + fn name(&self) -> &str { + self.name + } + + #[inline] + fn strict_clippy(&self) -> bool { + self.strict_clippy + } + + #[inline] + fn test(&self) -> bool { + self.test } } diff --git a/src/info_file.rs b/src/info_file.rs index 5ea487fc..f226f735 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Error, Result}; use serde::Deserialize; use std::{fs, io::ErrorKind}; -use crate::embedded::EMBEDDED_FILES; +use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; /// Deserialized from the `info.toml` file. #[derive(Deserialize)] @@ -75,6 +75,23 @@ impl ExerciseInfo { } } +impl RunnableExercise for ExerciseInfo { + #[inline] + fn name(&self) -> &str { + &self.name + } + + #[inline] + fn strict_clippy(&self) -> bool { + self.strict_clippy + } + + #[inline] + fn test(&self) -> bool { + self.test + } +} + /// The deserialized `info.toml` file. #[derive(Deserialize)] pub struct InfoFile { diff --git a/src/run.rs b/src/run.rs index 36899b91..899d0a94 100644 --- a/src/run.rs +++ b/src/run.rs @@ -4,14 +4,14 @@ use std::io::{self, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, - exercise::OUTPUT_CAPACITY, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, terminal_link::TerminalFileLink, }; pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run(&mut output, app_state.target_dir())?; + let success = exercise.run_exercise(&mut output, app_state.target_dir())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 14c3f015..dd43c566 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -8,7 +8,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ app_state::{AppState, ExercisesProgress}, clear_terminal, - exercise::OUTPUT_CAPACITY, + exercise::{RunnableExercise, OUTPUT_CAPACITY}, progress_bar::progress_bar, terminal_link::TerminalFileLink, }; @@ -54,7 +54,7 @@ impl<'a> WatchState<'a> { let success = self .app_state .current_exercise() - .run(&mut self.output, self.app_state.target_dir())?; + .run_exercise(&mut self.output, self.app_state.target_dir())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? { From 50530fa3cff5bc765291603873728799930d764b Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 21:50:11 +0200 Subject: [PATCH 327/433] Don't try to check a solution that doesn't exist --- src/dev/check.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dev/check.rs b/src/dev/check.rs index 6a3597cc..78396a8b 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -166,6 +166,11 @@ fn check_solutions(info_file: &InfoFile) -> Result<()> { let mut output = Vec::with_capacity(OUTPUT_CAPACITY); for exercise_info in &info_file.exercises { + if !Path::new(&exercise_info.sol_path()).exists() { + // No solution to check. + continue; + } + let success = exercise_info.run_solution(&mut output, &target_dir)?; if !success { io::stderr().write_all(&output)?; From a3ada0eee8b207870dd3774f23afbc5c4e2b3f12 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 1 Jun 2024 21:51:45 +0200 Subject: [PATCH 328/433] Print the exercise solution on check --- src/dev/check.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/check.rs b/src/dev/check.rs index 78396a8b..61b14190 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -171,6 +171,7 @@ fn check_solutions(info_file: &InfoFile) -> Result<()> { continue; } + println!("Running the solution of {}", exercise_info.name); let success = exercise_info.run_solution(&mut output, &target_dir)?; if !success { io::stderr().write_all(&output)?; From 6ae4a979f48301d259666129d2138291cd21246a Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 2 Jun 2024 00:03:48 +0200 Subject: [PATCH 329/433] Check for unexpected files in the solutions dir --- src/dev/check.rs | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 61b14190..15ff0880 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail, Context, Error, Result}; +use anyhow::{anyhow, bail, Context, Result}; use std::{ cmp::Ordering, fs::{self, read_dir, OpenOptions}, @@ -99,14 +99,19 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result) -> Result<()> { - fn unexpected_file(path: &Path) -> Error { - anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `exercises` directory", path.display()) - } +// Check `dir` for unexpected files. +// Only Rust files in `allowed_rust_files` and `README.md` files are allowed. +// Only one level of directory nesting is allowed. +fn check_unexpected_files( + dir: &str, + allowed_rust_files: &hashbrown::HashSet, +) -> Result<()> { + let unexpected_file = |path: &Path| { + anyhow!("Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory", path.display()) + }; - for entry in read_dir("exercises").context("Failed to open the `exercises` directory")? { - let entry = entry.context("Failed to read the `exercises` directory")?; + for entry in read_dir(dir).with_context(|| format!("Failed to open the `{dir}` directory"))? { + let entry = entry.with_context(|| format!("Failed to read the `{dir}` directory"))?; if entry.file_type().unwrap().is_file() { let path = entry.path(); @@ -115,7 +120,7 @@ fn check_unexpected_files(info_file_paths: &hashbrown::HashSet) -> Resu continue; } - if !info_file_paths.contains(&path) { + if !allowed_rust_files.contains(&path) { return Err(unexpected_file(&path)); } @@ -139,7 +144,7 @@ fn check_unexpected_files(info_file_paths: &hashbrown::HashSet) -> Resu continue; } - if !info_file_paths.contains(&path) { + if !allowed_rust_files.contains(&path) { return Err(unexpected_file(&path)); } } @@ -156,17 +161,19 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { } let info_file_paths = check_info_file_exercises(info_file)?; - check_unexpected_files(&info_file_paths)?; + check_unexpected_files("exercises", &info_file_paths)?; Ok(()) } fn check_solutions(info_file: &InfoFile) -> Result<()> { + let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); let target_dir = parse_target_dir()?; let mut output = Vec::with_capacity(OUTPUT_CAPACITY); for exercise_info in &info_file.exercises { - if !Path::new(&exercise_info.sol_path()).exists() { + let path = exercise_info.sol_path(); + if !Path::new(&path).exists() { // No solution to check. continue; } @@ -181,8 +188,12 @@ fn check_solutions(info_file: &InfoFile) -> Result<()> { exercise_info.name, ); } + + paths.insert(PathBuf::from(path)); } + check_unexpected_files("solutions", &paths)?; + Ok(()) } From 08ac11ff2250190a47a74a767c2efa3a71ce1e73 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 2 Jun 2024 00:11:41 +0200 Subject: [PATCH 330/433] Add --require-solutions option to `dev check` --- src/dev.rs | 8 ++++++-- src/dev/check.rs | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/dev.rs b/src/dev.rs index fada8b33..5f7e64c8 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -19,7 +19,11 @@ pub enum DevCommands { no_git: bool, }, /// Run checks on the exercises - Check, + Check { + /// Require that every exercise has a solution + #[arg(short, long)] + require_solutions: bool, + }, /// Update the `Cargo.toml` file for the exercises Update, } @@ -34,7 +38,7 @@ impl DevCommands { new::new(&path, no_git).context(INIT_ERR) } - Self::Check => check::check(), + Self::Check { require_solutions } => check::check(require_solutions), Self::Update => update::update(), } } diff --git a/src/dev/check.rs b/src/dev/check.rs index 15ff0880..ef45cd28 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -166,7 +166,7 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { Ok(()) } -fn check_solutions(info_file: &InfoFile) -> Result<()> { +fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> { let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); let target_dir = parse_target_dir()?; let mut output = Vec::with_capacity(OUTPUT_CAPACITY); @@ -174,6 +174,10 @@ fn check_solutions(info_file: &InfoFile) -> Result<()> { for exercise_info in &info_file.exercises { let path = exercise_info.sol_path(); if !Path::new(&path).exists() { + if require_solutions { + bail!("Exercise {} is missing a solution", exercise_info.name); + } + // No solution to check. continue; } @@ -197,7 +201,7 @@ fn check_solutions(info_file: &InfoFile) -> Result<()> { Ok(()) } -pub fn check() -> Result<()> { +pub fn check(require_solutions: bool) -> Result<()> { let info_file = InfoFile::parse()?; // A hack to make `cargo run -- dev check` work when developing Rustlings. @@ -214,7 +218,7 @@ pub fn check() -> Result<()> { } check_exercises(&info_file)?; - check_solutions(&info_file)?; + check_solutions(require_solutions, &info_file)?; println!("\nEverything looks fine!"); From 42bd0b8b75cbec30cb506f42295413ba0a6a1680 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 6 Jun 2024 01:58:05 +0200 Subject: [PATCH 331/433] Update deps --- Cargo.lock | 38 ++++++++++++++++---------------------- Cargo.toml | 4 ++-- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fec0365a..fc37b23e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -177,7 +177,7 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn", @@ -312,12 +312,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -496,9 +490,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_pipe" -version = "1.1.5" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" dependencies = [ "libc", "windows-sys 0.52.0", @@ -565,9 +559,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -812,11 +806,11 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.4.1", + "heck", "proc-macro2", "quote", "rustversion", @@ -851,9 +845,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", @@ -886,9 +880,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "utf8parse" @@ -1099,9 +1093,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6" +checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 90e329eb..8845027d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ edition = "2021" [workspace.dependencies] serde = { version = "1.0.203", features = ["derive"] } -toml_edit = { version = "0.22.13", default-features = false, features = ["parse", "serde"] } +toml_edit = { version = "0.22.14", default-features = false, features = ["parse", "serde"] } [package] name = "rustlings" @@ -51,7 +51,7 @@ clap = { version = "4.5.4", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } -os_pipe = "1.1.5" +os_pipe = "1.2.0" ratatui = { version = "0.26.3", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" } serde_json = "1.0.117" From f8d38320cd239d0abbd1b24fc972fd6f7f592ed9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 6 Jun 2024 01:59:09 +0200 Subject: [PATCH 332/433] Fix typos --- exercises/02_functions/functions5.rs | 2 +- solutions/02_functions/functions5.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/02_functions/functions5.rs b/exercises/02_functions/functions5.rs index 31fd057a..34a2ac7d 100644 --- a/exercises/02_functions/functions5.rs +++ b/exercises/02_functions/functions5.rs @@ -1,4 +1,4 @@ -// TODO: Fix the function body without chaning the signature. +// TODO: Fix the function body without changing the signature. fn square(num: i32) -> i32 { num * num; } diff --git a/solutions/02_functions/functions5.rs b/solutions/02_functions/functions5.rs index 03354186..677f3278 100644 --- a/solutions/02_functions/functions5.rs +++ b/solutions/02_functions/functions5.rs @@ -1,5 +1,5 @@ fn square(num: i32) -> i32 { - // Removed the semicolon `;` at the end of the line below to implicitely return the result. + // Removed the semicolon `;` at the end of the line below to implicitly return the result. num * num } From 0e4136d31e9aff282532b1a85cf40013bfba3f27 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 8 Jun 2024 21:19:16 +0200 Subject: [PATCH 333/433] Update deps --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc37b23e..58183ad2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.4" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7" dependencies = [ "clap_builder", "clap_derive", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df" dependencies = [ "anstream", "anstyle", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck", "proc-macro2", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -886,9 +886,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" @@ -1093,9 +1093,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.11" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c52728401e1dc672a56e81e593e912aa54c78f40246869f78359a2bf24d29d" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 8845027d..6ca25d7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ include = [ [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.4", features = ["derive"] } +clap = { version = "4.5.6", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } From e1051724c3f8d9dc3d25bcb854e0c4ac7ff3b2b6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 8 Jun 2024 21:35:44 +0200 Subject: [PATCH 334/433] primitive_types2 solution --- .../04_primitive_types/primitive_types1.rs | 2 ++ .../04_primitive_types/primitive_types2.rs | 13 ++++++----- .../04_primitive_types/primitive_types2.rs | 22 ++++++++++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/exercises/04_primitive_types/primitive_types1.rs b/exercises/04_primitive_types/primitive_types1.rs index 750d6e55..84923c75 100644 --- a/exercises/04_primitive_types/primitive_types1.rs +++ b/exercises/04_primitive_types/primitive_types1.rs @@ -1,3 +1,5 @@ +// Booleans (`bool`) + fn main() { let is_morning = true; if is_morning { diff --git a/exercises/04_primitive_types/primitive_types2.rs b/exercises/04_primitive_types/primitive_types2.rs index 29c74718..14018475 100644 --- a/exercises/04_primitive_types/primitive_types2.rs +++ b/exercises/04_primitive_types/primitive_types2.rs @@ -1,6 +1,6 @@ -fn main() { - // Characters (`char`) +// Characters (`char`) +fn main() { // Note the _single_ quotes, these are different from the double quotes // you've been seeing around. let my_first_initial = 'C'; @@ -12,9 +12,12 @@ fn main() { println!("Neither alphabetic nor numeric!"); } - let // Finish this line like the example! What's your favorite character? - // Try a letter, try a number, try a special character, try a character - // from a different language than your own, try an emoji! + // TODO: Analogous to the example before, declare a variable called `your_character` + // below with your favorite character. + // Try a letter, try a digit (in single quotes), try a special character, try a character + // from a different language than your own, try an emoji 😉 + // let your_character = ''; + if your_character.is_alphabetic() { println!("Alphabetical!"); } else if your_character.is_numeric() { diff --git a/solutions/04_primitive_types/primitive_types2.rs b/solutions/04_primitive_types/primitive_types2.rs index 4e181989..eecc6802 100644 --- a/solutions/04_primitive_types/primitive_types2.rs +++ b/solutions/04_primitive_types/primitive_types2.rs @@ -1 +1,21 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + let my_first_initial = 'C'; + if my_first_initial.is_alphabetic() { + println!("Alphabetical!"); + } else if my_first_initial.is_numeric() { + println!("Numerical!"); + } else { + println!("Neither alphabetic nor numeric!"); + } + + // Example with an emoji. + let your_character = '🦀'; + + if your_character.is_alphabetic() { + println!("Alphabetical!"); + } else if your_character.is_numeric() { + println!("Numerical!"); + } else { + println!("Neither alphabetic nor numeric!"); + } +} From 0338b1cbdfa567d5f9580afef1d4483c7d275c32 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 8 Jun 2024 21:43:38 +0200 Subject: [PATCH 335/433] primitive_types3 solution --- exercises/04_primitive_types/primitive_types3.rs | 7 +++---- rustlings-macros/info.toml | 2 +- solutions/04_primitive_types/primitive_types3.rs | 12 +++++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs index 5095fc4a..bef5579e 100644 --- a/exercises/04_primitive_types/primitive_types3.rs +++ b/exercises/04_primitive_types/primitive_types3.rs @@ -1,12 +1,11 @@ -// Create an array with at least 100 elements in it where the ??? is. - fn main() { - let a = ??? + // TODO: Create an array with at least 100 elements in it where the ??? is. + // let a = ??? if a.len() >= 100 { println!("Wow, that's a big array!"); } else { println!("Meh, I eat arrays like that for breakfast."); - panic!("Array not big enough, more elements needed") + panic!("Array not big enough, more elements needed"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 59de7f25..fc0bee80 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -250,7 +250,7 @@ name = "primitive_types3" dir = "04_primitive_types" test = false hint = """ -There's a shorthand to initialize Arrays with a certain size that does not +There's a shorthand to initialize arrays with a certain size that doesn't require you to type in 100 items (but you certainly can if you want!). For example, you can do: diff --git a/solutions/04_primitive_types/primitive_types3.rs b/solutions/04_primitive_types/primitive_types3.rs index 4e181989..8dd109f9 100644 --- a/solutions/04_primitive_types/primitive_types3.rs +++ b/solutions/04_primitive_types/primitive_types3.rs @@ -1 +1,11 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // An array with 100 elements of the value 42. + let a = [42; 100]; + + if a.len() >= 100 { + println!("Wow, that's a big array!"); + } else { + println!("Meh, I eat arrays like that for breakfast."); + panic!("Array not big enough, more elements needed"); + } +} From 98db5790144a0d32009718e54051b3f7d54ae494 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 8 Jun 2024 23:42:15 +0200 Subject: [PATCH 336/433] primitive_types4 solution --- .../04_primitive_types/primitive_types3.rs | 2 +- .../04_primitive_types/primitive_types4.rs | 7 ++---- rustlings-macros/info.toml | 4 ++-- .../04_primitive_types/primitive_types4.rs | 24 ++++++++++++++++++- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/exercises/04_primitive_types/primitive_types3.rs b/exercises/04_primitive_types/primitive_types3.rs index bef5579e..9b79c0cf 100644 --- a/exercises/04_primitive_types/primitive_types3.rs +++ b/exercises/04_primitive_types/primitive_types3.rs @@ -1,5 +1,5 @@ fn main() { - // TODO: Create an array with at least 100 elements in it where the ??? is. + // TODO: Create an array called `a` with at least 100 elements in it. // let a = ??? if a.len() >= 100 { diff --git a/exercises/04_primitive_types/primitive_types4.rs b/exercises/04_primitive_types/primitive_types4.rs index 661e0515..16e4fd93 100644 --- a/exercises/04_primitive_types/primitive_types4.rs +++ b/exercises/04_primitive_types/primitive_types4.rs @@ -1,18 +1,15 @@ -// Get a slice out of Array a where the ??? is so that the test passes. - fn main() { // You can optionally experiment here. } #[cfg(test)] mod tests { - use super::*; - #[test] fn slice_out_of_array() { let a = [1, 2, 3, 4, 5]; - let nice_slice = ??? + // TODO: Get a slice called `nice_slice` out of the array `a` so that the test passes. + // let nice_slice = ??? assert_eq!([2, 3, 4], nice_slice); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index fc0bee80..313ba6f7 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -267,8 +267,8 @@ dir = "04_primitive_types" hint = """ Take a look at the 'Understanding Ownership -> Slices -> Other Slices' section of the book: https://doc.rust-lang.org/book/ch04-03-slices.html and use the -starting and ending (plus one) indices of the items in the `Array` that you -want to end up in the slice. +starting and ending (plus one) indices of the items in the array that you want +to end up in the slice. If you're curious why the first argument of `assert_eq!` does not have an ampersand for a reference since the second argument is a reference, take a look diff --git a/solutions/04_primitive_types/primitive_types4.rs b/solutions/04_primitive_types/primitive_types4.rs index 4e181989..4807e66c 100644 --- a/solutions/04_primitive_types/primitive_types4.rs +++ b/solutions/04_primitive_types/primitive_types4.rs @@ -1 +1,23 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn slice_out_of_array() { + let a = [1, 2, 3, 4, 5]; + // 0 1 2 3 4 <- indices + // ------- + // | + // +--- slice + + // Note that the upper index 4 is excluded. + let nice_slice = &a[1..4]; + assert_eq!([2, 3, 4], nice_slice); + + // The upper index can be included by using the syntax `..=` (with `=` sign) + let nice_slice = &a[1..=3]; + assert_eq!([2, 3, 4], nice_slice); + } +} From 42a35039067861e82f5ec16901d12cb888834f09 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 10 Jun 2024 17:42:11 +0200 Subject: [PATCH 337/433] Run solutions in parallel --- src/dev/check.rs | 70 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index ef45cd28..336360be 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -4,6 +4,11 @@ use std::{ fs::{self, read_dir, OpenOptions}, io::{self, Read, Write}, path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Mutex, + }, + thread, }; use crate::{ @@ -167,36 +172,52 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { } fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> { - let mut paths = hashbrown::HashSet::with_capacity(info_file.exercises.len()); let target_dir = parse_target_dir()?; - let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len())); + let error_occured = AtomicBool::new(false); - for exercise_info in &info_file.exercises { - let path = exercise_info.sol_path(); - if !Path::new(&path).exists() { - if require_solutions { - bail!("Exercise {} is missing a solution", exercise_info.name); - } + println!("Running all solutions. This may take a while...\n"); + thread::scope(|s| { + for exercise_info in &info_file.exercises { + s.spawn(|| { + let error = |e| { + let mut stderr = io::stderr().lock(); + stderr.write_all(e).unwrap(); + stderr + .write_all(b"\nFailed to run the solution of the exercise ") + .unwrap(); + stderr.write_all(exercise_info.name.as_bytes()).unwrap(); + stderr.write_all(SEPARATOR).unwrap(); + error_occured.store(true, atomic::Ordering::Relaxed); + }; - // No solution to check. - continue; + let path = exercise_info.sol_path(); + if !Path::new(&path).exists() { + if require_solutions { + error(b"Solution missing"); + } + + // No solution to check. + return; + } + + let mut output = Vec::with_capacity(OUTPUT_CAPACITY); + match exercise_info.run_solution(&mut output, &target_dir) { + Ok(true) => { + paths.lock().unwrap().insert(PathBuf::from(path)); + } + Ok(false) => error(&output), + Err(e) => error(e.to_string().as_bytes()), + } + }); } + }); - println!("Running the solution of {}", exercise_info.name); - let success = exercise_info.run_solution(&mut output, &target_dir)?; - if !success { - io::stderr().write_all(&output)?; - - bail!( - "Failed to run the solution of the exercise {}", - exercise_info.name, - ); - } - - paths.insert(PathBuf::from(path)); + if error_occured.load(atomic::Ordering::Relaxed) { + bail!("At least one solution failed. See the output above."); } - check_unexpected_files("solutions", &paths)?; + check_unexpected_files("solutions", &paths.into_inner().unwrap())?; Ok(()) } @@ -224,3 +245,6 @@ pub fn check(require_solutions: bool) -> Result<()> { Ok(()) } + +const SEPARATOR: &[u8] = + b"\n========================================================================================\n"; From 2ff18137462513883533a10c929fe59364b8f884 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 14 Jun 2024 13:32:02 +0200 Subject: [PATCH 338/433] Update deps --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58183ad2..40b23f66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.6" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9689a29b593160de5bc4aacab7b5d54fb52231de70122626c178e6a368994c7" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.6" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5387378c84f6faa26890ebf9f0a92989f8873d4d380467bcd0d8d8620424df" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mio" @@ -615,9 +615,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -627,9 +627,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -638,9 +638,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustlings" diff --git a/Cargo.toml b/Cargo.toml index 6ca25d7d..8e67312d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ include = [ [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.6", features = ["derive"] } +clap = { version = "4.5.7", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } From 5bf8d1fa1bcbf885c7cd9c7ae49494826c209da6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 14 Jun 2024 13:32:37 +0200 Subject: [PATCH 339/433] Fix typos --- exercises/01_variables/variables5.rs | 2 +- src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/01_variables/variables5.rs b/exercises/01_variables/variables5.rs index 73f655e3..49db8e9e 100644 --- a/exercises/01_variables/variables5.rs +++ b/exercises/01_variables/variables5.rs @@ -2,7 +2,7 @@ fn main() { let number = "T-H-R-E-E"; // Don't change this line println!("Spell a number: {}", number); - // TODO: Fix the compiler error by changing the line below without renaming the the variable. + // TODO: Fix the compiler error by changing the line below without renaming the variable. number = 3; println!("Number plus two is: {}", number + 2); } diff --git a/src/main.rs b/src/main.rs index cf6f0d96..2233d8b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -151,7 +151,7 @@ fn main() -> Result<()> { let notify_exercise_names = if args.manual_run { None } else { - // For the the notify event handler thread. + // For the notify event handler thread. // Leaking is not a problem because the slice lives until the end of the program. Some( &*app_state From 2a1bc5377177981fa253ae4cb234bce584ce1b62 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 19 Jun 2024 14:03:06 +0200 Subject: [PATCH 340/433] Update deps --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40b23f66..82c06f9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,7 +516,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", "windows-targets 0.52.5", ] @@ -606,9 +606,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags 2.5.0", ] From 532c9ebb30afa226590e68e87af11da42b598974 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 19 Jun 2024 14:17:06 +0200 Subject: [PATCH 341/433] primitive_types5 solution --- exercises/04_primitive_types/primitive_types5.rs | 8 ++++---- rustlings-macros/info.toml | 2 +- solutions/04_primitive_types/primitive_types5.rs | 9 ++++++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/exercises/04_primitive_types/primitive_types5.rs b/exercises/04_primitive_types/primitive_types5.rs index f2216a55..6e00ef51 100644 --- a/exercises/04_primitive_types/primitive_types5.rs +++ b/exercises/04_primitive_types/primitive_types5.rs @@ -1,8 +1,8 @@ -// Destructure the `cat` tuple so that the println will work. - fn main() { let cat = ("Furry McFurson", 3.5); - let /* your pattern here */ = cat; - println!("{} is {} years old.", name, age); + // TODO: Destructure the `cat` tuple in one statement so that the println works. + // let /* your pattern here */ = cat; + + println!("{name} is {age} years old"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 313ba6f7..e5f58777 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -286,7 +286,7 @@ Particularly the part about destructuring (second to last example in the section). You'll need to make a pattern to bind `name` and `age` to the appropriate parts -of the tuple. You can do it!!""" +of the tuple.""" [[exercises]] name = "primitive_types6" diff --git a/solutions/04_primitive_types/primitive_types5.rs b/solutions/04_primitive_types/primitive_types5.rs index 4e181989..46d7ae87 100644 --- a/solutions/04_primitive_types/primitive_types5.rs +++ b/solutions/04_primitive_types/primitive_types5.rs @@ -1 +1,8 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + let cat = ("Furry McFurson", 3.5); + + // Destructuring the tuple. + let (name, age) = cat; + + println!("{name} is {age} years old"); +} From 0abcdeed42957ca805a3a7475fb3f14085af346e Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 19 Jun 2024 14:25:29 +0200 Subject: [PATCH 342/433] primitive_types6 solution --- .../04_primitive_types/primitive_types6.rs | 14 +++++--------- rustlings-macros/info.toml | 2 +- .../04_primitive_types/primitive_types6.rs | 17 ++++++++++++++++- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/exercises/04_primitive_types/primitive_types6.rs b/exercises/04_primitive_types/primitive_types6.rs index 83cec24b..a97e5311 100644 --- a/exercises/04_primitive_types/primitive_types6.rs +++ b/exercises/04_primitive_types/primitive_types6.rs @@ -1,21 +1,17 @@ -// Use a tuple index to access the second element of `numbers`. You can put the -// expression for the second element where ??? is so that the test passes. - fn main() { // You can optionally experiment here. } #[cfg(test)] mod tests { - use super::*; - #[test] fn indexing_tuple() { let numbers = (1, 2, 3); - // Replace below ??? with the tuple indexing syntax. - let second = ???; - assert_eq!(2, second, - "This is not the 2nd number in the tuple!") + // TODO: Use a tuple index to access the second element of `numbers` + // and assign it to a variable called `second`. + // let second = ???; + + assert_eq!(second, 2, "This is not the 2nd number in the tuple!"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index e5f58777..cd85dcc3 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -296,7 +296,7 @@ While you could use a destructuring `let` for the tuple here, try indexing into it instead, as explained in the last example of the 'Data Types -> The Tuple Type' section of the book: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type -Now you have another tool in your toolbox!""" +Now, you have another tool in your toolbox!""" # VECS diff --git a/solutions/04_primitive_types/primitive_types6.rs b/solutions/04_primitive_types/primitive_types6.rs index 4e181989..9b7c2779 100644 --- a/solutions/04_primitive_types/primitive_types6.rs +++ b/solutions/04_primitive_types/primitive_types6.rs @@ -1 +1,16 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn indexing_tuple() { + let numbers = (1, 2, 3); + + // Tuple indexing syntax. + let second = numbers.1; + + assert_eq!(second, 2, "This is not the 2nd number in the tuple!"); + } +} From a9f0c7bf1f00ab19733953d3121d462eede34466 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 20 Jun 2024 01:00:06 +0200 Subject: [PATCH 343/433] vecs1 solution --- exercises/05_vecs/vecs1.rs | 14 ++++++-------- rustlings-macros/info.toml | 5 +++-- solutions/05_vecs/vecs1.rs | 24 +++++++++++++++++++++++- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/exercises/05_vecs/vecs1.rs b/exercises/05_vecs/vecs1.rs index ddcad84b..68e1affa 100644 --- a/exercises/05_vecs/vecs1.rs +++ b/exercises/05_vecs/vecs1.rs @@ -1,11 +1,9 @@ -// Your task is to create a `Vec` which holds the exact same elements as in the -// array `a`. -// -// Make me compile and pass the test! - fn array_and_vec() -> ([i32; 4], Vec) { - let a = [10, 20, 30, 40]; // a plain array - let v = // TODO: declare your vector here with the macro for vectors + let a = [10, 20, 30, 40]; // Array + + // TODO: Create a vector called `v` which contains the exact same elements as in the array `a`. + // Use the vector macro. + // let v = ???; (a, v) } @@ -21,6 +19,6 @@ mod tests { #[test] fn test_array_and_vec_similarity() { let (a, v) = array_and_vec(); - assert_eq!(a, v[..]); + assert_eq!(a, *v); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index cd85dcc3..21a27dd1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -307,8 +307,9 @@ hint = """ In Rust, there are two ways to define a Vector. 1. One way is to use the `Vec::new()` function to create a new vector and fill it with the `push()` method. -2. The second way, which is simpler is to use the `vec![]` macro and - define your elements inside the square brackets. +2. The second way is to use the `vec![]` macro and define your elements + inside the square brackets. This way is simpler when you exactly know + the initial values. Check this chapter: https://doc.rust-lang.org/stable/book/ch08-01-vectors.html of the Rust book to learn more. diff --git a/solutions/05_vecs/vecs1.rs b/solutions/05_vecs/vecs1.rs index 4e181989..55b5676c 100644 --- a/solutions/05_vecs/vecs1.rs +++ b/solutions/05_vecs/vecs1.rs @@ -1 +1,23 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn array_and_vec() -> ([i32; 4], Vec) { + let a = [10, 20, 30, 40]; // Array + + // Used the `vec!` macro. + let v = vec![10, 20, 30, 40]; + + (a, v) +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_array_and_vec_similarity() { + let (a, v) = array_and_vec(); + assert_eq!(a, *v); + } +} From 835ec7262247a341295c1d6f3772901a5fad5148 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 14:52:11 +0200 Subject: [PATCH 344/433] vecs2 solution + significant change to have a better comparison between both methods --- exercises/05_vecs/vecs2.rs | 60 +++++++++++++++++++++++--------------- rustlings-macros/info.toml | 11 ++----- solutions/05_vecs/vecs2.rs | 51 +++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 33 deletions(-) diff --git a/exercises/05_vecs/vecs2.rs b/exercises/05_vecs/vecs2.rs index e72209c4..a9be2580 100644 --- a/exercises/05_vecs/vecs2.rs +++ b/exercises/05_vecs/vecs2.rs @@ -1,25 +1,32 @@ -// A Vec of even numbers is given. Your task is to complete the loop so that -// each number in the Vec is multiplied by 2. -// -// Make me pass the test! +fn vec_loop(input: &[i32]) -> Vec { + let mut output = Vec::new(); -fn vec_loop(mut v: Vec) -> Vec { - for element in v.iter_mut() { - // TODO: Fill this up so that each element in the Vec `v` is - // multiplied by 2. - ??? + for element in input { + // TODO: Multiply each element in the `input` slice by 2 and push it to + // the `output` vector. } - // At this point, `v` should be equal to [4, 8, 12, 16, 20]. - v + output } -fn vec_map(v: &Vec) -> Vec { - v.iter().map(|element| { - // TODO: Do the same thing as above - but instead of mutating the - // Vec, you can just return the new number! - ??? - }).collect() +fn vec_map_example(input: &[i32]) -> Vec { + // An example of collecting a vector after mapping. + // We map each element of the `input` slice to its value plus 1. + // If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`. + input.iter().map(|element| element + 1).collect() +} + +fn vec_map(input: &[i32]) -> Vec { + // TODO: Here, we also want to multiply each element in the `input` slice + // by 2, but with iterator mapping instead of manually pushing into an empty + // vector. + // See the example in the function `vec_map_example` above. + input + .iter() + .map(|element| { + // ??? + }) + .collect() } fn main() { @@ -32,17 +39,22 @@ mod tests { #[test] fn test_vec_loop() { - let v: Vec = (1..).filter(|x| x % 2 == 0).take(5).collect(); - let ans = vec_loop(v.clone()); + let input = [2, 4, 6, 8, 10]; + let ans = vec_loop(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); + } - assert_eq!(ans, v.iter().map(|x| x * 2).collect::>()); + #[test] + fn test_vec_map_example() { + let input = [1, 2, 3]; + let ans = vec_map_example(&input); + assert_eq!(ans, [2, 3, 4]); } #[test] fn test_vec_map() { - let v: Vec = (1..).filter(|x| x % 2 == 0).take(5).collect(); - let ans = vec_map(&v); - - assert_eq!(ans, v.iter().map(|x| x * 2).collect::>()); + let input = [2, 4, 6, 8, 10]; + let ans = vec_map(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 21a27dd1..3edd1c67 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -319,15 +319,10 @@ of the Rust book to learn more. name = "vecs2" dir = "05_vecs" hint = """ -In the first function we are looping over the Vector and getting a reference to -one `element` at a time. +In the first function, we create an empty vector and want to push new elements +to it. -To modify the value of that `element` we need to use the `*` dereference -operator. You can learn more in this chapter of the Rust book: -https://doc.rust-lang.org/stable/book/ch08-01-vectors.html#iterating-over-the-values-in-a-vector - -In the second function this dereferencing is not necessary, because the `map` -function expects the new value to be returned. +In the second function, we map the values of the input and collect them into a vector. After you've completed both functions, decide for yourself which approach you like better. diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs index 4e181989..32c1c0f6 100644 --- a/solutions/05_vecs/vecs2.rs +++ b/solutions/05_vecs/vecs2.rs @@ -1 +1,50 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn vec_loop(input: &[i32]) -> Vec { + let mut output = Vec::new(); + + for element in input { + output.push(2 * element); + } + + output +} + +fn vec_map_example(input: &[i32]) -> Vec { + // An example of collecting a vector after mapping. + // We map each element of the `input` slice to its value plus 1. + // If the input is `[1, 2, 3]`, the output is `[2, 3, 4]`. + input.iter().map(|element| element + 1).collect() +} + +fn vec_map(input: &[i32]) -> Vec { + input.iter().map(|element| 2 * element).collect() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vec_loop() { + let input = [2, 4, 6, 8, 10]; + let ans = vec_loop(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); + } + + #[test] + fn test_vec_map_example() { + let input = [1, 2, 3]; + let ans = vec_map_example(&input); + assert_eq!(ans, [2, 3, 4]); + } + + #[test] + fn test_vec_map() { + let input = [2, 4, 6, 8, 10]; + let ans = vec_map(&input); + assert_eq!(ans, [4, 8, 12, 16, 20]); + } +} From 6a79ada7f2afc668418c8fd15e2db622f3d833af Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 15:06:50 +0200 Subject: [PATCH 345/433] Add comment to vecs2 --- solutions/05_vecs/vecs2.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solutions/05_vecs/vecs2.rs b/solutions/05_vecs/vecs2.rs index 32c1c0f6..87f7625a 100644 --- a/solutions/05_vecs/vecs2.rs +++ b/solutions/05_vecs/vecs2.rs @@ -16,6 +16,11 @@ fn vec_map_example(input: &[i32]) -> Vec { } fn vec_map(input: &[i32]) -> Vec { + // We will dive deeper into iterators, but for now, this is all what you + // had to do! + // Advanced note: This method is more efficient because it automatically + // preallocates enough capacity. This can be done manually in `vec_loop` + // using `Vec::with_capacity(input.len())` instead of `Vec::new()`. input.iter().map(|element| 2 * element).collect() } From 946c29679e27433ff455bdb30343551757d87769 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 16:16:52 +0200 Subject: [PATCH 346/433] move_semantics1 solution --- .../06_move_semantics/move_semantics1.rs | 3 +-- rustlings-macros/info.toml | 3 +-- .../06_move_semantics/move_semantics1.rs | 26 ++++++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/exercises/06_move_semantics/move_semantics1.rs b/exercises/06_move_semantics/move_semantics1.rs index 8c3fe3a7..4eb3d618 100644 --- a/exercises/06_move_semantics/move_semantics1.rs +++ b/exercises/06_move_semantics/move_semantics1.rs @@ -1,3 +1,4 @@ +// TODO: Fix the compiler error in this function. fn fill_vec(vec: Vec) -> Vec { let vec = vec; @@ -17,9 +18,7 @@ mod tests { #[test] fn move_semantics1() { let vec0 = vec![22, 44, 66]; - let vec1 = fill_vec(vec0); - assert_eq!(vec1, vec![22, 44, 66, 88]); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 3edd1c67..bfe32cdd 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -342,8 +342,7 @@ error on the line where we push an element to the vector, right? The fix for this is going to be adding one keyword, and the addition is NOT on the line where we push to the vector (where the error is). -Also: Try accessing `vec0` after having called `fill_vec()`. See what -happens!""" +Try accessing `vec0` after having called `fill_vec()`. See what happens!""" [[exercises]] name = "move_semantics2" diff --git a/solutions/06_move_semantics/move_semantics1.rs b/solutions/06_move_semantics/move_semantics1.rs index 4e181989..ac34e7a0 100644 --- a/solutions/06_move_semantics/move_semantics1.rs +++ b/solutions/06_move_semantics/move_semantics1.rs @@ -1 +1,25 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn fill_vec(vec: Vec) -> Vec { + let mut vec = vec; + // ^^^ added + + vec.push(88); + + vec +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics1() { + let vec0 = vec![22, 44, 66]; + let vec1 = fill_vec(vec0); + // `vec0` can't be accessed anymore because it is moved to `fill_vec`. + assert_eq!(vec1, vec![22, 44, 66, 88]); + } +} From 68142aff7f439f3a797b4e97a275ca7800eebc45 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 17:02:50 +0200 Subject: [PATCH 347/433] move_semantics2 solution --- .../06_move_semantics/move_semantics2.rs | 8 ++--- rustlings-macros/info.toml | 12 ++------ .../06_move_semantics/move_semantics2.rs | 29 ++++++++++++++++++- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/exercises/06_move_semantics/move_semantics2.rs b/exercises/06_move_semantics/move_semantics2.rs index d0879113..a3ab7a0f 100644 --- a/exercises/06_move_semantics/move_semantics2.rs +++ b/exercises/06_move_semantics/move_semantics2.rs @@ -1,5 +1,3 @@ -// Make the test pass by finding a way to keep both Vecs separate! - fn fill_vec(vec: Vec) -> Vec { let mut vec = vec; @@ -16,13 +14,15 @@ fn main() { mod tests { use super::*; + // TODO: Make both vectors `vec0` and `vec1` accessible at the same time to + // fix the compiler error in the test. #[test] fn move_semantics2() { let vec0 = vec![22, 44, 66]; let vec1 = fill_vec(vec0); - assert_eq!(vec0, vec![22, 44, 66]); - assert_eq!(vec1, vec![22, 44, 66, 88]); + assert_eq!(vec0, [22, 44, 66]); + assert_eq!(vec1, [22, 44, 66, 88]); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index bfe32cdd..fb0126c1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -352,16 +352,10 @@ When running this exercise for the first time, you'll notice an error about "borrow of moved value". In Rust, when an argument is passed to a function and it's not explicitly returned, you can't use the original variable anymore. We call this "moving" a variable. When we pass `vec0` into `fill_vec`, it's -being "moved" into `vec1`, meaning we can't access `vec0` anymore after the -fact. +being "moved" into `vec1`, meaning we can't access `vec0` anymore. -Rust provides a couple of different ways to mitigate this issue, feel free to -try them all: -1. You could make another, separate version of the data that's in `vec0` and - pass that to `fill_vec` instead. -2. Make `fill_vec` borrow its argument instead of taking ownership of it, - and then copy the data within the function (`vec.clone()`) in order to - return an owned `Vec`. +You could make another, separate version of the data that's in `vec0` and +pass it to `fill_vec` instead. """ [[exercises]] diff --git a/solutions/06_move_semantics/move_semantics2.rs b/solutions/06_move_semantics/move_semantics2.rs index 4e181989..7bcd33a0 100644 --- a/solutions/06_move_semantics/move_semantics2.rs +++ b/solutions/06_move_semantics/move_semantics2.rs @@ -1 +1,28 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn fill_vec(vec: Vec) -> Vec { + let mut vec = vec; + + vec.push(88); + + vec +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics2() { + let vec0 = vec![22, 44, 66]; + + // Cloning `vec0` so that the clone is moved into `fill_vec`, not `vec0` + // itself. + let vec1 = fill_vec(vec0.clone()); + + assert_eq!(vec0, [22, 44, 66]); + assert_eq!(vec1, [22, 44, 66, 88]); + } +} From fd558065c7f32d38d1b8e34fda1a23fe40d1b3ab Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 17:04:51 +0200 Subject: [PATCH 348/433] move_semantics3 solution --- .../06_move_semantics/move_semantics3.rs | 8 ++----- .../06_move_semantics/move_semantics3.rs | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/exercises/06_move_semantics/move_semantics3.rs b/exercises/06_move_semantics/move_semantics3.rs index 24e35971..11dbbbeb 100644 --- a/exercises/06_move_semantics/move_semantics3.rs +++ b/exercises/06_move_semantics/move_semantics3.rs @@ -1,6 +1,4 @@ -// Make me compile without adding new lines -- just changing existing lines! (no -// lines with multiple semicolons necessary!) - +// TODO: Fix the compiler error in the function without adding any new line. fn fill_vec(vec: Vec) -> Vec { vec.push(88); @@ -18,9 +16,7 @@ mod tests { #[test] fn move_semantics3() { let vec0 = vec![22, 44, 66]; - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); + assert_eq!(vec1, [22, 44, 66, 88]); } } diff --git a/solutions/06_move_semantics/move_semantics3.rs b/solutions/06_move_semantics/move_semantics3.rs index 4e181989..7ba4006b 100644 --- a/solutions/06_move_semantics/move_semantics3.rs +++ b/solutions/06_move_semantics/move_semantics3.rs @@ -1 +1,22 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn fill_vec(mut vec: Vec) -> Vec { + // ^^^ added + vec.push(88); + + vec +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn move_semantics3() { + let vec0 = vec![22, 44, 66]; + let vec1 = fill_vec(vec0); + assert_eq!(vec1, [22, 44, 66, 88]); + } +} From e4dbbbf5f5f5d4ea0ede1ead1f82108968e6cea6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 18:14:19 +0200 Subject: [PATCH 349/433] Remove move_semantics4, add rest of move_semantics solutions --- .../06_move_semantics/move_semantics4.rs | 31 +++++------------ .../06_move_semantics/move_semantics5.rs | 33 ++++++++++--------- .../06_move_semantics/move_semantics6.rs | 21 ------------ rustlings-macros/info.toml | 27 +++------------ .../06_move_semantics/move_semantics4.rs | 22 ++++++++++++- .../06_move_semantics/move_semantics5.rs | 22 ++++++++++++- .../06_move_semantics/move_semantics6.rs | 1 - 7 files changed, 73 insertions(+), 84 deletions(-) delete mode 100644 exercises/06_move_semantics/move_semantics6.rs delete mode 100644 solutions/06_move_semantics/move_semantics6.rs diff --git a/exercises/06_move_semantics/move_semantics4.rs b/exercises/06_move_semantics/move_semantics4.rs index b6622244..c225f3ba 100644 --- a/exercises/06_move_semantics/move_semantics4.rs +++ b/exercises/06_move_semantics/move_semantics4.rs @@ -1,31 +1,18 @@ -// Refactor this code so that instead of passing `vec0` into the `fill_vec` -// function, the Vector gets created in the function itself and passed back to -// the test function. - -// `fill_vec()` no longer takes `vec: Vec` as argument - don't change this! -fn fill_vec() -> Vec { - // Instead, let's create and fill the Vec in here - how do you do that? - let mut vec = vec; - - vec.push(88); - - vec -} - fn main() { // You can optionally experiment here. } #[cfg(test)] mod tests { - use super::*; - + // TODO: Fix the compiler errors only by reordering the lines in the test. + // Don't add, change or remove any line. #[test] - fn move_semantics4() { - let vec0 = vec![22, 44, 66]; - - let vec1 = fill_vec(vec0); - - assert_eq!(vec1, vec![22, 44, 66, 88]); + fn move_semantics5() { + let mut x = 100; + let y = &mut x; + let z = &mut x; + *y += 100; + *z += 1000; + assert_eq!(x, 1200); } } diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs index b34560ab..c9edf41d 100644 --- a/exercises/06_move_semantics/move_semantics5.rs +++ b/exercises/06_move_semantics/move_semantics5.rs @@ -1,21 +1,22 @@ -// Make me compile only by reordering the lines in the test, but without adding, -// changing or removing any of them. +// TODO: Fix the compiler erros. Don't change anything except adding or removing +// references (the character `&`). fn main() { - // You can optionally experiment here. + let data = "Rust is great!".to_string(); + + get_char(data); + + string_uppercase(&data); } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn move_semantics5() { - let mut x = 100; - let y = &mut x; - let z = &mut x; - *y += 100; - *z += 1000; - assert_eq!(x, 1200); - } +// Shouldn't take ownership +fn get_char(data: String) -> char { + data.chars().last().unwrap() +} + +// Should take ownership +fn string_uppercase(mut data: &String) { + data = &data.to_uppercase(); + + println!("{data}"); } diff --git a/exercises/06_move_semantics/move_semantics6.rs b/exercises/06_move_semantics/move_semantics6.rs deleted file mode 100644 index 2ad71db2..00000000 --- a/exercises/06_move_semantics/move_semantics6.rs +++ /dev/null @@ -1,21 +0,0 @@ -// You can't change anything except adding or removing references. - -fn main() { - let data = "Rust is great!".to_string(); - - get_char(data); - - string_uppercase(&data); -} - -// Should not take ownership -fn get_char(data: String) -> char { - data.chars().last().unwrap() -} - -// Should take ownership -fn string_uppercase(mut data: &String) { - data = &data.to_uppercase(); - - println!("{}", data); -} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index fb0126c1..d6236c51 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -371,28 +371,15 @@ an existing binding to be a mutable binding instead of an immutable one :)""" name = "move_semantics4" dir = "06_move_semantics" hint = """ -Stop reading whenever you feel like you have enough direction :) Or try -doing one step and then fixing the compiler errors that result! -So the end goal is to: - - get rid of the first line in main that creates the new vector - - so then `vec0` doesn't exist, so we can't pass it to `fill_vec` - - `fill_vec` has had its signature changed, which our call should reflect - - since we're not creating a new vec in `main` anymore, we need to create - a new vec in `fill_vec`, and fill it with the expected values""" - -[[exercises]] -name = "move_semantics5" -dir = "06_move_semantics" -hint = """ Carefully reason about the range in which each mutable reference is in scope. Does it help to update the value of referent (`x`) immediately after -the mutable reference is taken? Read more about 'Mutable References' -in the book's section 'References and Borrowing': +the mutable reference is taken? +Read more about 'Mutable References' in the book's section 'References and Borrowing': https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#mutable-references. """ [[exercises]] -name = "move_semantics6" +name = "move_semantics5" dir = "06_move_semantics" test = false hint = """ @@ -401,14 +388,10 @@ https://doc.rust-lang.org/stable/book/ch04-02-references-and-borrowing.html The first problem is that `get_char` is taking ownership of the string. So `data` is moved and can't be used for `string_uppercase`. `data` is moved to -`get_char` first, meaning that `string_uppercase` cannot manipulate the data. +`get_char` first, meaning that `string_uppercase` can't manipulate the data. Once you've fixed that, `string_uppercase`'s function signature will also need -to be adjusted. - -Can you figure out how? - -Another hint: it has to do with the `&` character.""" +to be adjusted.""" # STRUCTS diff --git a/solutions/06_move_semantics/move_semantics4.rs b/solutions/06_move_semantics/move_semantics4.rs index 4e181989..b7919ac0 100644 --- a/solutions/06_move_semantics/move_semantics4.rs +++ b/solutions/06_move_semantics/move_semantics4.rs @@ -1 +1,21 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // TODO: Fix the compiler errors only by reordering the lines in the test. + // Don't add, change or remove any line. + #[test] + fn move_semantics5() { + let mut x = 100; + let y = &mut x; + // `y` used here. + *y += 100; + // The mutable reference `y` is not used anymore, + // therefore a new reference can be created. + let z = &mut x; + *z += 1000; + assert_eq!(x, 1200); + } +} diff --git a/solutions/06_move_semantics/move_semantics5.rs b/solutions/06_move_semantics/move_semantics5.rs index 4e181989..1b3ca4eb 100644 --- a/solutions/06_move_semantics/move_semantics5.rs +++ b/solutions/06_move_semantics/move_semantics5.rs @@ -1 +1,21 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + let data = "Rust is great!".to_string(); + + get_char(&data); + + string_uppercase(data); +} + +// Borrows instead of taking ownership. +// It is recommended to use `&str` instead of `&String` here. But this is +// enough for now because we didn't handle strings yet. +fn get_char(data: &String) -> char { + data.chars().last().unwrap() +} + +// Takes ownership instead of borrowing. +fn string_uppercase(mut data: String) { + data = data.to_uppercase(); + + println!("{data}"); +} diff --git a/solutions/06_move_semantics/move_semantics6.rs b/solutions/06_move_semantics/move_semantics6.rs deleted file mode 100644 index 4e181989..00000000 --- a/solutions/06_move_semantics/move_semantics6.rs +++ /dev/null @@ -1 +0,0 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 From d768353806f905989b4cc29cd7a97891cbbf8ec3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 18:29:00 +0200 Subject: [PATCH 350/433] Fix typo --- exercises/06_move_semantics/move_semantics5.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/06_move_semantics/move_semantics5.rs b/exercises/06_move_semantics/move_semantics5.rs index c9edf41d..65065688 100644 --- a/exercises/06_move_semantics/move_semantics5.rs +++ b/exercises/06_move_semantics/move_semantics5.rs @@ -1,5 +1,5 @@ -// TODO: Fix the compiler erros. Don't change anything except adding or removing -// references (the character `&`). +// TODO: Fix the compiler errors without changing anything except adding or +// removing references (the character `&`). fn main() { let data = "Rust is great!".to_string(); From ef842d3a946f477a32e26b9674cc5488cd629030 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 22:22:37 +0200 Subject: [PATCH 351/433] structs1 solution --- exercises/07_structs/structs1.rs | 25 ++++++++-------- rustlings-macros/info.toml | 9 +++--- solutions/07_structs/structs1.rs | 50 +++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/exercises/07_structs/structs1.rs b/exercises/07_structs/structs1.rs index 62f14219..959c4c6a 100644 --- a/exercises/07_structs/structs1.rs +++ b/exercises/07_structs/structs1.rs @@ -1,13 +1,12 @@ -// Address all the TODOs to make the tests pass! - -struct ColorClassicStruct { - // TODO: Something goes here +struct ColorRegularStruct { + // TODO: Add the fields that the test `regular_structs` expects. + // What types should the fields have? What are the minimum and maximum values for RGB colors? } -struct ColorTupleStruct(/* TODO: Something goes here */); +struct ColorTupleStruct(/* TODO: Add the fields that the test `tuple_structs` expects */); #[derive(Debug)] -struct UnitLikeStruct; +struct UnitStruct; fn main() { // You can optionally experiment here. @@ -18,8 +17,8 @@ mod tests { use super::*; #[test] - fn classic_c_structs() { - // TODO: Instantiate a classic c struct! + fn regular_structs() { + // TODO: Instantiate a regular struct. // let green = assert_eq!(green.red, 0); @@ -29,7 +28,7 @@ mod tests { #[test] fn tuple_structs() { - // TODO: Instantiate a tuple struct! + // TODO: Instantiate a tuple struct. // let green = assert_eq!(green.0, 0); @@ -39,10 +38,10 @@ mod tests { #[test] fn unit_structs() { - // TODO: Instantiate a unit-like struct! - // let unit_like_struct = - let message = format!("{:?}s are fun!", unit_like_struct); + // TODO: Instantiate a unit struct. + // let unit_struct = + let message = format!("{unit_struct:?}s are fun!"); - assert_eq!(message, "UnitLikeStructs are fun!"); + assert_eq!(message, "UnitStructs are fun!"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index d6236c51..81b98952 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -402,15 +402,14 @@ hint = """ Rust has more than one type of struct. Three actually, all variants are used to package related data together. -There are normal (or classic) structs. These are named collections of related -data stored in fields. +There are regular structs. These are named collections of related data stored in +fields. Tuple structs are basically just named tuples. -Finally, Unit-like structs. These don't have any fields and are useful for -generics. +Finally, unit structs. These don't have any fields and are useful for generics. -In this exercise you need to complete and implement one of each kind. +In this exercise, you need to complete and implement one of each kind. Read more about structs in The Book: https://doc.rust-lang.org/book/ch05-01-defining-structs.html""" diff --git a/solutions/07_structs/structs1.rs b/solutions/07_structs/structs1.rs index 4e181989..98fafcc5 100644 --- a/solutions/07_structs/structs1.rs +++ b/solutions/07_structs/structs1.rs @@ -1 +1,49 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +struct ColorRegularStruct { + red: u8, + green: u8, + blue: u8, +} + +struct ColorTupleStruct(u8, u8, u8); + +#[derive(Debug)] +struct UnitStruct; + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn regular_structs() { + let green = ColorRegularStruct { + red: 0, + green: 255, + blue: 0, + }; + + assert_eq!(green.red, 0); + assert_eq!(green.green, 255); + assert_eq!(green.blue, 0); + } + + #[test] + fn tuple_structs() { + let green = ColorTupleStruct(0, 255, 0); + + assert_eq!(green.0, 0); + assert_eq!(green.1, 255); + assert_eq!(green.2, 0); + } + + #[test] + fn unit_structs() { + let unit_struct = UnitStruct; + let message = format!("{unit_struct:?}s are fun!"); + + assert_eq!(message, "UnitStructs are fun!"); + } +} From 1264510fc04de85efc0e2caf17aaa85354b6bffd Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 22:31:06 +0200 Subject: [PATCH 352/433] structs2 solution --- exercises/07_structs/structs2.rs | 4 +-- rustlings-macros/info.toml | 2 +- solutions/07_structs/structs2.rs | 52 +++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/exercises/07_structs/structs2.rs b/exercises/07_structs/structs2.rs index 451dbe76..79141af9 100644 --- a/exercises/07_structs/structs2.rs +++ b/exercises/07_structs/structs2.rs @@ -1,5 +1,3 @@ -// Address all the TODOs to make the tests pass! - #[derive(Debug)] struct Order { name: String, @@ -34,8 +32,10 @@ mod tests { #[test] fn your_order() { let order_template = create_order_template(); + // TODO: Create your own order using the update syntax and template above! // let your_order = + assert_eq!(your_order.name, "Hacker in Rust"); assert_eq!(your_order.year, order_template.year); assert_eq!(your_order.made_by_phone, order_template.made_by_phone); diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 81b98952..08bbd74d 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -421,7 +421,7 @@ Creating instances of structs is easy, all you need to do is assign some values to its fields. There are however some shortcuts that can be taken when instantiating structs. -Have a look in The Book, to find out more: +Have a look in The Book to find out more: https://doc.rust-lang.org/stable/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax""" [[exercises]] diff --git a/solutions/07_structs/structs2.rs b/solutions/07_structs/structs2.rs index 4e181989..589dd933 100644 --- a/solutions/07_structs/structs2.rs +++ b/solutions/07_structs/structs2.rs @@ -1 +1,51 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(Debug)] +struct Order { + name: String, + year: u32, + made_by_phone: bool, + made_by_mobile: bool, + made_by_email: bool, + item_number: u32, + count: u32, +} + +fn create_order_template() -> Order { + Order { + name: String::from("Bob"), + year: 2019, + made_by_phone: false, + made_by_mobile: false, + made_by_email: true, + item_number: 123, + count: 0, + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn your_order() { + let order_template = create_order_template(); + + let your_order = Order { + name: String::from("Hacker in Rust"), + count: 1, + // Struct update syntax + ..order_template + }; + + assert_eq!(your_order.name, "Hacker in Rust"); + assert_eq!(your_order.year, order_template.year); + assert_eq!(your_order.made_by_phone, order_template.made_by_phone); + assert_eq!(your_order.made_by_mobile, order_template.made_by_mobile); + assert_eq!(your_order.made_by_email, order_template.made_by_email); + assert_eq!(your_order.item_number, order_template.item_number); + assert_eq!(your_order.count, 1); + } +} From d6fd251a73f2abd96662b09b32f718021568675c Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 22:54:00 +0200 Subject: [PATCH 353/433] structs3 solution --- exercises/07_structs/structs3.rs | 33 +++++++------ rustlings-macros/info.toml | 2 +- solutions/07_structs/structs3.rs | 84 +++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 18 deletions(-) diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 10adb487..18a6cc98 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -1,6 +1,5 @@ // Structs contain data, but can also have logic. In this exercise we have -// defined the Package struct and we want to test some logic attached to it. -// Make the code compile and the tests pass! +// defined the `Package` struct and we want to test some logic attached to it. #[derive(Debug)] struct Package { @@ -10,26 +9,28 @@ struct Package { } impl Package { - fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Package { + fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { if weight_in_grams < 10 { - // This is not how you should handle errors in Rust, - // but we will learn about error handling later. - panic!("Can not ship a package with weight below 10 grams.") - } else { - Package { - sender_country, - recipient_country, - weight_in_grams, - } + // This isn't how you should handle errors in Rust, but we will + // learn about error handling later. + panic!("Can't ship a package with weight below 10 grams"); + } + + Self { + sender_country, + recipient_country, + weight_in_grams, } } - fn is_international(&self) -> ??? { - // Something goes here... + // TODO: Add the correct return type to the function signature. + fn is_international(&self) { + // TODO: Read the tests that use this method to find out when a package is concidered international. } - fn get_fees(&self, cents_per_gram: u32) -> ??? { - // Something goes here... + // TODO: Add the correct return type to the function signature. + fn get_fees(&self, cents_per_gram: u32) { + // TODO: Calculate the package's fees. } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 08bbd74d..7535b682 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -434,7 +434,7 @@ the places it goes through right? For `get_fees`: This method takes an additional argument, is there a field in the `Package` struct that this relates to? -Have a look in The Book, to find out more about method implementations: +Have a look in The Book to find out more about method implementations: https://doc.rust-lang.org/book/ch05-03-method-syntax.html""" # ENUMS diff --git a/solutions/07_structs/structs3.rs b/solutions/07_structs/structs3.rs index 4e181989..3f878cc8 100644 --- a/solutions/07_structs/structs3.rs +++ b/solutions/07_structs/structs3.rs @@ -1 +1,83 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(Debug)] +struct Package { + sender_country: String, + recipient_country: String, + weight_in_grams: u32, +} + +impl Package { + fn new(sender_country: String, recipient_country: String, weight_in_grams: u32) -> Self { + if weight_in_grams < 10 { + // This isn't how you should handle errors in Rust, but we will + // learn about error handling later. + panic!("Can't ship a package with weight below 10 grams"); + } + + Self { + sender_country, + recipient_country, + weight_in_grams, + } + } + + fn is_international(&self) -> bool { + // ^^^^^^^ added + self.sender_country != self.recipient_country + } + + fn get_fees(&self, cents_per_gram: u32) -> u32 { + // ^^^^^^ added + self.weight_in_grams * cents_per_gram + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic] + fn fail_creating_weightless_package() { + let sender_country = String::from("Spain"); + let recipient_country = String::from("Austria"); + + Package::new(sender_country, recipient_country, 5); + } + + #[test] + fn create_international_package() { + let sender_country = String::from("Spain"); + let recipient_country = String::from("Russia"); + + let package = Package::new(sender_country, recipient_country, 1200); + + assert!(package.is_international()); + } + + #[test] + fn create_local_package() { + let sender_country = String::from("Canada"); + let recipient_country = sender_country.clone(); + + let package = Package::new(sender_country, recipient_country, 1200); + + assert!(!package.is_international()); + } + + #[test] + fn calculate_transport_fees() { + let sender_country = String::from("Spain"); + let recipient_country = String::from("Spain"); + + let cents_per_gram = 3; + + let package = Package::new(sender_country, recipient_country, 1500); + + assert_eq!(package.get_fees(cents_per_gram), 4500); + assert_eq!(package.get_fees(cents_per_gram * 2), 9000); + } +} From a2dfbd86da3271eb07b57a89a359ce2efdcc3544 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 23:00:38 +0200 Subject: [PATCH 354/433] enums1 solution --- exercises/08_enums/enums1.rs | 2 +- solutions/08_enums/enums1.rs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/exercises/08_enums/enums1.rs b/exercises/08_enums/enums1.rs index d63de83f..99f70874 100644 --- a/exercises/08_enums/enums1.rs +++ b/exercises/08_enums/enums1.rs @@ -1,6 +1,6 @@ #[derive(Debug)] enum Message { - // TODO: define a few types of messages as used below + // TODO: Define a few types of messages as used below. } fn main() { diff --git a/solutions/08_enums/enums1.rs b/solutions/08_enums/enums1.rs index 4e181989..97248834 100644 --- a/solutions/08_enums/enums1.rs +++ b/solutions/08_enums/enums1.rs @@ -1 +1,14 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(Debug)] +enum Message { + Quit, + Echo, + Move, + ChangeColor, +} + +fn main() { + println!("{:?}", Message::Quit); + println!("{:?}", Message::Echo); + println!("{:?}", Message::Move); + println!("{:?}", Message::ChangeColor); +} From 020711fa97e3be57f9e0098d6b9a329deec5a754 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 23:05:40 +0200 Subject: [PATCH 355/433] enums3 solution --- exercises/08_enums/enums2.rs | 2 +- rustlings-macros/info.toml | 2 +- solutions/08_enums/enums2.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/exercises/08_enums/enums2.rs b/exercises/08_enums/enums2.rs index f3b803ff..14aa29ad 100644 --- a/exercises/08_enums/enums2.rs +++ b/exercises/08_enums/enums2.rs @@ -1,6 +1,6 @@ #[derive(Debug)] enum Message { - // TODO: define the different variants used below + // TODO: Define the different variants used below. } impl Message { diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 7535b682..a9921045 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -451,7 +451,7 @@ dir = "08_enums" test = false hint = """ You can create enumerations that have different variants with different types -such as no data, anonymous structs, a single string, tuples, ...etc""" +such as no data, anonymous structs, a single string, tuples, etc.""" [[exercises]] name = "enums3" diff --git a/solutions/08_enums/enums2.rs b/solutions/08_enums/enums2.rs index 4e181989..13175dd3 100644 --- a/solutions/08_enums/enums2.rs +++ b/solutions/08_enums/enums2.rs @@ -1 +1,26 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(Debug)] +enum Message { + Move { x: i64, y: i64 }, + Echo(String), + ChangeColor(u8, u8, u8), + Quit, +} + +impl Message { + fn call(&self) { + println!("{:?}", self); + } +} + +fn main() { + let messages = [ + Message::Move { x: 10, y: 30 }, + Message::Echo(String::from("hello world")), + Message::ChangeColor(200, 255, 255), + Message::Quit, + ]; + + for message in &messages { + message.call(); + } +} From 2901d856627889e5a52dcf9f97d1c77032081c08 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 21 Jun 2024 23:18:25 +0200 Subject: [PATCH 356/433] enums3 solution --- exercises/08_enums/enums3.rs | 19 +++++---- rustlings-macros/info.toml | 6 +-- solutions/08_enums/enums3.rs | 76 +++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/exercises/08_enums/enums3.rs b/exercises/08_enums/enums3.rs index edac3dfb..7dd21715 100644 --- a/exercises/08_enums/enums3.rs +++ b/exercises/08_enums/enums3.rs @@ -1,7 +1,5 @@ -// Address all the TODOs to make the tests pass! - enum Message { - // TODO: implement the message variant types based on their usage below + // TODO: Implement the message variant types based on their usage below. } struct Point { @@ -26,17 +24,17 @@ impl State { } fn echo(&mut self, s: String) { - self.message = s + self.message = s; } - fn move_position(&mut self, p: Point) { - self.position = p; + fn move_position(&mut self, point: Point) { + self.position = point; } fn process(&mut self, message: Message) { - // TODO: create a match expression to process the different message variants + // TODO: Create a match expression to process the different message variants. // Remember: When passing a tuple as a function argument, you'll need extra parentheses: - // fn function((t, u, p, l, e)) + // e.g. `foo((t, u, p, l, e))` } } @@ -54,8 +52,9 @@ mod tests { quit: false, position: Point { x: 0, y: 0 }, color: (0, 0, 0), - message: "hello world".to_string(), + message: String::from("hello world"), }; + state.process(Message::ChangeColor(255, 0, 255)); state.process(Message::Echo(String::from("Hello world!"))); state.process(Message::Move(Point { x: 10, y: 15 })); @@ -64,7 +63,7 @@ mod tests { assert_eq!(state.color, (255, 0, 255)); assert_eq!(state.position.x, 10); assert_eq!(state.position.y, 15); - assert_eq!(state.quit, true); + assert!(state.quit); assert_eq!(state.message, "Hello world!"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index a9921045..46a2c4b1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -457,12 +457,12 @@ such as no data, anonymous structs, a single string, tuples, etc.""" name = "enums3" dir = "08_enums" hint = """ -As a first step, you can define enums to compile this code without errors. +As a first step, define enums to compile the code without errors. -And then create a match expression in `process()`. +Then, create a match expression in `process()`. Note that you need to deconstruct some message variants in the match expression -to get value in the variant.""" +to get the variant's values.""" # STRINGS diff --git a/solutions/08_enums/enums3.rs b/solutions/08_enums/enums3.rs index 4e181989..8baa25c1 100644 --- a/solutions/08_enums/enums3.rs +++ b/solutions/08_enums/enums3.rs @@ -1 +1,75 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +enum Message { + ChangeColor(u8, u8, u8), + Echo(String), + Move(Point), + Quit, +} + +struct Point { + x: u8, + y: u8, +} + +struct State { + color: (u8, u8, u8), + position: Point, + quit: bool, + message: String, +} + +impl State { + fn change_color(&mut self, color: (u8, u8, u8)) { + self.color = color; + } + + fn quit(&mut self) { + self.quit = true; + } + + fn echo(&mut self, s: String) { + self.message = s; + } + + fn move_position(&mut self, point: Point) { + self.position = point; + } + + fn process(&mut self, message: Message) { + match message { + Message::ChangeColor(r, g, b) => self.change_color((r, g, b)), + Message::Echo(s) => self.echo(s), + Message::Move(point) => self.move_position(point), + Message::Quit => self.quit(), + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_match_message_call() { + let mut state = State { + quit: false, + position: Point { x: 0, y: 0 }, + color: (0, 0, 0), + message: String::from("hello world"), + }; + + state.process(Message::ChangeColor(255, 0, 255)); + state.process(Message::Echo(String::from("Hello world!"))); + state.process(Message::Move(Point { x: 10, y: 15 })); + state.process(Message::Quit); + + assert_eq!(state.color, (255, 0, 255)); + assert_eq!(state.position.x, 10); + assert_eq!(state.position.y, 15); + assert!(state.quit); + assert_eq!(state.message, "Hello world!"); + } +} From bd63ece47cbb6bde9e2fe53db543a8d22a246f5e Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 12:05:28 +0200 Subject: [PATCH 357/433] string1 solution --- exercises/09_strings/strings1.rs | 13 ++++++------- solutions/09_strings/strings1.rs | 10 +++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/exercises/09_strings/strings1.rs b/exercises/09_strings/strings1.rs index de762ebf..6abdbb48 100644 --- a/exercises/09_strings/strings1.rs +++ b/exercises/09_strings/strings1.rs @@ -1,10 +1,9 @@ -// Make me compile without changing the function signature! - -fn main() { - let answer = current_favorite_color(); - println!("My current favorite color is {}", answer); -} - +// TODO: Fix the compiler error without changing the function signature. fn current_favorite_color() -> String { "blue" } + +fn main() { + let answer = current_favorite_color(); + println!("My current favorite color is {answer}"); +} diff --git a/solutions/09_strings/strings1.rs b/solutions/09_strings/strings1.rs index 4e181989..f7ba8114 100644 --- a/solutions/09_strings/strings1.rs +++ b/solutions/09_strings/strings1.rs @@ -1 +1,9 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn current_favorite_color() -> String { + // Equivalent to `String::from("blue")` + "blue".to_string() +} + +fn main() { + let answer = current_favorite_color(); + println!("My current favorite color is {answer}"); +} From f574905b8e7d3b9320b2cb3a4c18e2039c9a771f Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 12:14:04 +0200 Subject: [PATCH 358/433] strings2 solution --- exercises/09_strings/strings2.rs | 12 ++++++------ rustlings-macros/info.toml | 2 +- solutions/09_strings/strings2.rs | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/exercises/09_strings/strings2.rs b/exercises/09_strings/strings2.rs index 47682781..93d9cb6b 100644 --- a/exercises/09_strings/strings2.rs +++ b/exercises/09_strings/strings2.rs @@ -1,14 +1,14 @@ -// Make me compile without changing the function signature! +// TODO: Fix the compiler error in the `main` function without changing this function. +fn is_a_color_word(attempt: &str) -> bool { + attempt == "green" || attempt == "blue" || attempt == "red" +} fn main() { - let word = String::from("green"); // Try not changing this line :) + let word = String::from("green"); // Don't change this line. + if is_a_color_word(word) { println!("That is a color word I know!"); } else { println!("That is not a color word I know."); } } - -fn is_a_color_word(attempt: &str) -> bool { - attempt == "green" || attempt == "blue" || attempt == "red" -} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 46a2c4b1..82206fc7 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -486,7 +486,7 @@ dir = "09_strings" test = false hint = """ Yes, it would be really easy to fix this by just changing the value bound to -`word` to be a string slice instead of a `String`, wouldn't it?? There is a way +`word` to be a string slice instead of a `String`, wouldn't it? There is a way to add one character to the `if` statement, though, that will coerce the `String` into a string slice. diff --git a/solutions/09_strings/strings2.rs b/solutions/09_strings/strings2.rs index 4e181989..7de311ff 100644 --- a/solutions/09_strings/strings2.rs +++ b/solutions/09_strings/strings2.rs @@ -1 +1,15 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn is_a_color_word(attempt: &str) -> bool { + attempt == "green" || attempt == "blue" || attempt == "red" +} + +fn main() { + let word = String::from("green"); + + if is_a_color_word(&word) { + // ^ added to have `&String` which is automatically + // coerced to `&str` by the compiler. + println!("That is a color word I know!"); + } else { + println!("That is not a color word I know."); + } +} From 613ec23f84d49078ed2e63c6111b7cf30ee764d6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 12:22:24 +0200 Subject: [PATCH 359/433] strings 3 solution --- exercises/09_strings/strings3.rs | 21 ++++++++------ rustlings-macros/info.toml | 3 +- solutions/09_strings/strings3.rs | 49 +++++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/exercises/09_strings/strings3.rs b/exercises/09_strings/strings3.rs index f83a5310..39fce18c 100644 --- a/exercises/09_strings/strings3.rs +++ b/exercises/09_strings/strings3.rs @@ -1,16 +1,13 @@ -fn trim_me(input: &str) -> String { - // TODO: Remove whitespace from both ends of a string! - ??? +fn trim_me(input: &str) -> &str { + // TODO: Remove whitespace from both ends of a string. } fn compose_me(input: &str) -> String { - // TODO: Add " world!" to the string! There are multiple ways to do this! - ??? + // TODO: Add " world!" to the string! There are multiple ways to do this. } fn replace_me(input: &str) -> String { - // TODO: Replace "cars" in the string with "balloons"! - ??? + // TODO: Replace "cars" in the string with "balloons". } fn main() { @@ -36,7 +33,13 @@ mod tests { #[test] fn replace_a_string() { - assert_eq!(replace_me("I think cars are cool"), "I think balloons are cool"); - assert_eq!(replace_me("I love to look at cars"), "I love to look at balloons"); + assert_eq!( + replace_me("I think cars are cool"), + "I think balloons are cool", + ); + assert_eq!( + replace_me("I love to look at cars"), + "I love to look at balloons", + ); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 82206fc7..618fc918 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -499,7 +499,8 @@ https://doc.rust-lang.org/stable/book/ch15-02-deref.html#implicit-deref-coercion name = "strings3" dir = "09_strings" hint = """ -There's tons of useful standard library functions for strings. Let's try and use some of them: +There are many useful standard library functions for strings. Let's try and use +some of them: https://doc.rust-lang.org/std/string/struct.String.html#method.trim For the `compose_me` method: You can either use the `format!` macro, or convert diff --git a/solutions/09_strings/strings3.rs b/solutions/09_strings/strings3.rs index 4e181989..a478e62a 100644 --- a/solutions/09_strings/strings3.rs +++ b/solutions/09_strings/strings3.rs @@ -1 +1,48 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn trim_me(input: &str) -> &str { + input.trim() +} + +fn compose_me(input: &str) -> String { + // The macro `format!` has the same syntax as `println!`, but it returns a + // string instead of printing it to the terminal. + // Equivalent to `input.to_string() + " world!"` + format!("{input} world!") +} + +fn replace_me(input: &str) -> String { + input.replace("cars", "balloons") +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn trim_a_string() { + assert_eq!(trim_me("Hello! "), "Hello!"); + assert_eq!(trim_me(" What's up!"), "What's up!"); + assert_eq!(trim_me(" Hola! "), "Hola!"); + } + + #[test] + fn compose_a_string() { + assert_eq!(compose_me("Hello"), "Hello world!"); + assert_eq!(compose_me("Goodbye"), "Goodbye world!"); + } + + #[test] + fn replace_a_string() { + assert_eq!( + replace_me("I think cars are cool"), + "I think balloons are cool", + ); + assert_eq!( + replace_me("I love to look at cars"), + "I love to look at balloons", + ); + } +} From 879f0cd5c69b8b0bf93da036d31334f9757bf6a3 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 12:51:21 +0200 Subject: [PATCH 360/433] strings4 solution --- exercises/09_strings/strings4.rs | 44 ++++++++++++++++++++------------ rustlings-macros/info.toml | 10 +++++++- solutions/09_strings/strings4.rs | 39 +++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 18 deletions(-) diff --git a/exercises/09_strings/strings4.rs b/exercises/09_strings/strings4.rs index 1f3d88b7..9d9eb480 100644 --- a/exercises/09_strings/strings4.rs +++ b/exercises/09_strings/strings4.rs @@ -1,24 +1,36 @@ -// Ok, here are a bunch of values - some are `String`s, some are `&str`s. Your -// task is to call one of these two functions on each value depending on what -// you think each value is. That is, add either `string_slice` or `string` -// before the parentheses on each line. If you're right, it will compile! +// Calls of this function should be replaced with calls of `string_slice` or `string`. +fn placeholder() {} fn string_slice(arg: &str) { - println!("{}", arg); + println!("{arg}"); } fn string(arg: String) { - println!("{}", arg); + println!("{arg}"); } +// TODO: Here are a bunch of values - some are `String`, some are `&str`. +// Your task is to replace `placeholder(…)` with either `string_slice(…)` +// or `string(…)` depending on what you think each value is. fn main() { - ???("blue"); - ???("red".to_string()); - ???(String::from("hi")); - ???("rust is fun!".to_owned()); - ???("nice weather".into()); - ???(format!("Interpolation {}", "Station")); - ???(&String::from("abc")[0..1]); - ???(" hello there ".trim()); - ???("Happy Monday!".to_string().replace("Mon", "Tues")); - ???("mY sHiFt KeY iS sTiCkY".to_lowercase()); + placeholder("blue"); + + placeholder("red".to_string()); + + placeholder(String::from("hi")); + + placeholder("rust is fun!".to_owned()); + + placeholder("nice weather".into()); + + placeholder(format!("Interpolation {}", "Station")); + + // WARNING: This is byte indexing, not character indexing. + // Character indexing can be done using `s.chars().nth(INDEX)`. + placeholder(&String::from("abc")[0..1]); + + placeholder(" hello there ".trim()); + + placeholder("Happy Monday!".replace("Mon", "Tues")); + + placeholder("mY sHiFt KeY iS sTiCkY".to_lowercase()); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 618fc918..7607650b 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -510,7 +510,15 @@ the string slice into an owned string, which you can then freely extend.""" name = "strings4" dir = "09_strings" test = false -hint = "No hints this time ;)" +hint = """ +Replace `placeholder` with either `string` or `string_slice` in the `main` function. + +Example: +`placeholder("blue");` +should become +`string_slice("blue");` +because "blue" is `&str`, not `String`. +""" # MODULES diff --git a/solutions/09_strings/strings4.rs b/solutions/09_strings/strings4.rs index 4e181989..9dc6917e 100644 --- a/solutions/09_strings/strings4.rs +++ b/solutions/09_strings/strings4.rs @@ -1 +1,38 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn string_slice(arg: &str) { + println!("{arg}"); +} +fn string(arg: String) { + println!("{arg}"); +} + +fn main() { + string_slice("blue"); + + string("red".to_string()); + + string(String::from("hi")); + + string("rust is fun!".to_owned()); + + // Here, both answers work. + // `.into()` converts a type into an expected type. + // If it is called where `String` is expected, it will convert `&str` to `String`. + // But if is called where `&str` is expected, then `&str` is kept `&str` since no + // conversion is needed. + string("nice weather".into()); + string_slice("nice weather".into()); + // ^^^^^^^ the compiler recommends removing the `.into()` + // call because it is a useless conversion. + + string(format!("Interpolation {}", "Station")); + + // WARNING: This is byte indexing, not character indexing. + // Character indexing can be done using `s.chars().nth(INDEX)`. + string_slice(&String::from("abc")[0..1]); + + string_slice(" hello there ".trim()); + + string("Happy Monday!".replace("Mon", "Tues")); + + string("mY sHiFt KeY iS sTiCkY".to_lowercase()); +} From ecbe9b7324364e94f7c6b4a4dd279fb90f5a938e Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 13:12:39 +0200 Subject: [PATCH 361/433] modules1 solution --- exercises/10_modules/modules1.rs | 1 + rustlings-macros/info.toml | 5 ++--- solutions/10_modules/modules1.rs | 16 +++++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/exercises/10_modules/modules1.rs b/exercises/10_modules/modules1.rs index 931a3e26..d97ab23a 100644 --- a/exercises/10_modules/modules1.rs +++ b/exercises/10_modules/modules1.rs @@ -1,3 +1,4 @@ +// TODO: Fix the compiler error about calling a private function. mod sausage_factory { // Don't let anybody outside of this module see this! fn get_secret_recipe() -> String { diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 7607650b..97d7e07d 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -527,9 +527,8 @@ name = "modules1" dir = "10_modules" test = false hint = """ -Everything is private in Rust by default-- but there's a keyword we can use -to make something public! The compiler error should point to the thing that -needs to be public.""" +Everything is private in Rust by default. But there's a keyword we can use +to make something public!""" [[exercises]] name = "modules2" diff --git a/solutions/10_modules/modules1.rs b/solutions/10_modules/modules1.rs index 4e181989..873b4127 100644 --- a/solutions/10_modules/modules1.rs +++ b/solutions/10_modules/modules1.rs @@ -1 +1,15 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +mod sausage_factory { + fn get_secret_recipe() -> String { + String::from("Ginger") + } + + // Added `pub` before `fn` to make the function accessible outside the module. + pub fn make_sausage() { + get_secret_recipe(); + println!("sausage!"); + } +} + +fn main() { + sausage_factory::make_sausage(); +} From 98cd00de6378550985d819ac8cd1227c8a10818e Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 13:24:06 +0200 Subject: [PATCH 362/433] modules2 solution --- exercises/10_modules/modules2.rs | 19 +++++++++---------- rustlings-macros/info.toml | 11 ++++++----- solutions/10_modules/modules2.rs | 24 +++++++++++++++++++++++- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 5f8b0d51..24dce41f 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -1,20 +1,19 @@ // You can bring module paths into scopes and provide new names for them with -// the 'use' and 'as' keywords. Fix these 'use' statements to make the code -// compile. +// the `use` and `as` keywords. mod delicious_snacks { - // TODO: Fix these use statements - use self::fruits::PEAR as ??? - use self::veggies::CUCUMBER as ??? + // TODO: Add the follwing two `use` statements after fixing them. + // use self::fruits::PEAR as ???; + // use self::veggies::CUCUMBER as ???; mod fruits { - pub const PEAR: &'static str = "Pear"; - pub const APPLE: &'static str = "Apple"; + pub const PEAR: &str = "Pear"; + pub const APPLE: &str = "Apple"; } mod veggies { - pub const CUCUMBER: &'static str = "Cucumber"; - pub const CARROT: &'static str = "Carrot"; + pub const CUCUMBER: &str = "Cucumber"; + pub const CARROT: &str = "Carrot"; } } @@ -22,6 +21,6 @@ fn main() { println!( "favorite snacks: {} and {}", delicious_snacks::fruit, - delicious_snacks::veggie + delicious_snacks::veggie, ); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 97d7e07d..ba414e34 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -535,12 +535,13 @@ name = "modules2" dir = "10_modules" test = false hint = """ -The delicious_snacks module is trying to present an external interface that is -different than its internal structure (the `fruits` and `veggies` modules and -associated constants). Complete the `use` statements to fit the uses in main and -find the one keyword missing for both constants. +The `delicious_snacks` module is trying to present an external interface that +is different than its internal structure (the `fruits` and `veggies` modules +and associated constants). Complete the `use` statements to fit the uses in +`main` and find the one keyword missing for both constants. -Learn more at https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use""" +Learn more in The Book: +https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#re-exporting-names-with-pub-use""" [[exercises]] name = "modules3" diff --git a/solutions/10_modules/modules2.rs b/solutions/10_modules/modules2.rs index 4e181989..55c316d7 100644 --- a/solutions/10_modules/modules2.rs +++ b/solutions/10_modules/modules2.rs @@ -1 +1,23 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +mod delicious_snacks { + // Added `pub` and used the expected alias after `as`. + pub use self::fruits::PEAR as fruit; + pub use self::veggies::CUCUMBER as veggie; + + mod fruits { + pub const PEAR: &str = "Pear"; + pub const APPLE: &str = "Apple"; + } + + mod veggies { + pub const CUCUMBER: &str = "Cucumber"; + pub const CARROT: &str = "Carrot"; + } +} + +fn main() { + println!( + "favorite snacks: {} and {}", + delicious_snacks::fruit, + delicious_snacks::veggie, + ); +} From 3d540ed946ee9fd522ba9ec26f68055f5c498317 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 22 Jun 2024 13:35:54 +0200 Subject: [PATCH 363/433] modules3 solution --- exercises/10_modules/modules3.rs | 11 +++++------ rustlings-macros/info.toml | 2 +- solutions/10_modules/modules3.rs | 9 ++++++++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/exercises/10_modules/modules3.rs b/exercises/10_modules/modules3.rs index eff24a9c..691608d2 100644 --- a/exercises/10_modules/modules3.rs +++ b/exercises/10_modules/modules3.rs @@ -1,10 +1,9 @@ -// You can use the 'use' keyword to bring module paths from modules from -// anywhere and especially from the Rust standard library into your scope. Bring -// SystemTime and UNIX_EPOCH from the std::time module. Bonus style points if -// you can do it with one line! +// You can use the `use` keyword to bring module paths from modules from +// anywhere and especially from the standard library into your scope. -// TODO: Complete this use statement -use ??? +// TODO: Bring `SystemTime` and `UNIX_EPOCH` from the `std::time` module into +// your scope. Bonus style points if you can do it with one line! +// use ???; fn main() { match SystemTime::now().duration_since(UNIX_EPOCH) { diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ba414e34..58c0cdd5 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -550,7 +550,7 @@ test = false hint = """ `UNIX_EPOCH` and `SystemTime` are declared in the `std::time` module. Add a `use` statement for these two to bring them into scope. You can use nested -paths or the glob operator to bring these two in using only one line.""" +paths to bring these two in using only one line.""" # HASHMAPS diff --git a/solutions/10_modules/modules3.rs b/solutions/10_modules/modules3.rs index 4e181989..99ff5a77 100644 --- a/solutions/10_modules/modules3.rs +++ b/solutions/10_modules/modules3.rs @@ -1 +1,8 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +use std::time::{SystemTime, UNIX_EPOCH}; + +fn main() { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(n) => println!("1970-01-01 00:00:00 UTC was {} seconds ago!", n.as_secs()), + Err(_) => panic!("SystemTime before UNIX EPOCH!"), + } +} From 5baa503bfc27fc691dbc292b46d37d25c17cffab Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 24 Jun 2024 13:20:50 +0200 Subject: [PATCH 364/433] hashmaps1 solution --- exercises/11_hashmaps/hashmaps1.rs | 11 ++++---- rustlings-macros/info.toml | 8 ++---- solutions/11_hashmaps/hashmaps1.rs | 43 +++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps1.rs b/exercises/11_hashmaps/hashmaps1.rs index e646ed71..0df70000 100644 --- a/exercises/11_hashmaps/hashmaps1.rs +++ b/exercises/11_hashmaps/hashmaps1.rs @@ -1,20 +1,19 @@ // A basket of fruits in the form of a hash map needs to be defined. The key // represents the name of the fruit and the value represents how many of that -// particular fruit is in the basket. You have to put at least three different +// particular fruit is in the basket. You have to put at least 3 different // types of fruits (e.g apple, banana, mango) in the basket and the total count -// of all the fruits should be at least five. -// -// Make me compile and pass the tests! +// of all the fruits should be at least 5. use std::collections::HashMap; fn fruit_basket() -> HashMap { - let mut basket = // TODO: declare your hash map here. + // TODO: Declare the hash map. + // let mut basket = // Two bananas are already given for you :) basket.insert(String::from("banana"), 2); - // TODO: Put more fruits in your basket here. + // TODO: Put more fruits in your basket. basket } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 58c0cdd5..cf70d4d4 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -558,12 +558,8 @@ paths to bring these two in using only one line.""" name = "hashmaps1" dir = "11_hashmaps" hint = """ -Hint 1: Take a look at the return type of the function to figure out - the type for the `basket`. - -Hint 2: Number of fruits should be at least 5. And you have to put - at least three different types of fruits. -""" +The number of fruits should be at least 5 and you have to put at least 3 +different types of fruits.""" [[exercises]] name = "hashmaps2" diff --git a/solutions/11_hashmaps/hashmaps1.rs b/solutions/11_hashmaps/hashmaps1.rs index 4e181989..3a787c43 100644 --- a/solutions/11_hashmaps/hashmaps1.rs +++ b/solutions/11_hashmaps/hashmaps1.rs @@ -1 +1,42 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// A basket of fruits in the form of a hash map needs to be defined. The key +// represents the name of the fruit and the value represents how many of that +// particular fruit is in the basket. You have to put at least 3 different +// types of fruits (e.g apple, banana, mango) in the basket and the total count +// of all the fruits should be at least 5. + +use std::collections::HashMap; + +fn fruit_basket() -> HashMap { + // Declare the hash map. + let mut basket = HashMap::new(); + + // Two bananas are already given for you :) + basket.insert(String::from("banana"), 2); + + // Put more fruits in your basket. + basket.insert(String::from("apple"), 3); + basket.insert(String::from("mango"), 1); + + basket +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn at_least_three_types_of_fruits() { + let basket = fruit_basket(); + assert!(basket.len() >= 3); + } + + #[test] + fn at_least_five_fruits() { + let basket = fruit_basket(); + assert!(basket.values().sum::() >= 5); + } +} From fbc226a51043f7c9be4c414292d37d3ce97038fe Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 24 Jun 2024 16:50:03 +0200 Subject: [PATCH 365/433] hashmaps2 solution --- exercises/11_hashmaps/hashmaps2.rs | 23 +++---- rustlings-macros/info.toml | 5 +- solutions/11_hashmaps/hashmaps2.rs | 96 +++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps2.rs b/exercises/11_hashmaps/hashmaps2.rs index 05b7a870..b3691b68 100644 --- a/exercises/11_hashmaps/hashmaps2.rs +++ b/exercises/11_hashmaps/hashmaps2.rs @@ -6,8 +6,6 @@ // must add fruit to the basket so that there is at least one of each kind and // more than 11 in total - we have a lot of mouths to feed. You are not allowed // to insert any more of these fruits! -// -// Make me pass the tests! use std::collections::HashMap; @@ -21,7 +19,7 @@ enum Fruit { } fn fruit_basket(basket: &mut HashMap) { - let fruit_kinds = vec![ + let fruit_kinds = [ Fruit::Apple, Fruit::Banana, Fruit::Mango, @@ -46,12 +44,8 @@ mod tests { // Don't modify this function! fn get_fruit_basket() -> HashMap { - let mut basket = HashMap::::new(); - basket.insert(Fruit::Apple, 4); - basket.insert(Fruit::Mango, 2); - basket.insert(Fruit::Lychee, 5); - - basket + let content = [(Fruit::Apple, 4), (Fruit::Mango, 2), (Fruit::Lychee, 5)]; + HashMap::from_iter(content) } #[test] @@ -81,7 +75,7 @@ mod tests { #[test] fn all_fruit_types_in_basket() { - let fruit_kinds = vec![ + let fruit_kinds = [ Fruit::Apple, Fruit::Banana, Fruit::Mango, @@ -91,11 +85,12 @@ mod tests { let mut basket = get_fruit_basket(); fruit_basket(&mut basket); + for fruit_kind in fruit_kinds { - let amount = basket - .get(&fruit_kind) - .expect(format!("Fruit kind {:?} was not found in basket", fruit_kind).as_str()); - assert_ne!(amount, &0); + let Some(amount) = basket.get(&fruit_kind) else { + panic!("Fruit kind {fruit_kind:?} was not found in basket"); + }; + assert!(*amount > 0); } } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index cf70d4d4..0da573be 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -566,8 +566,9 @@ name = "hashmaps2" dir = "11_hashmaps" hint = """ Use the `entry()` and `or_insert()` methods of `HashMap` to achieve this. -Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value -""" + +Learn more in The Book: +https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value""" [[exercises]] name = "hashmaps3" diff --git a/solutions/11_hashmaps/hashmaps2.rs b/solutions/11_hashmaps/hashmaps2.rs index 4e181989..a5e6ef9b 100644 --- a/solutions/11_hashmaps/hashmaps2.rs +++ b/solutions/11_hashmaps/hashmaps2.rs @@ -1 +1,95 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// We're collecting different fruits to bake a delicious fruit cake. For this, +// we have a basket, which we'll represent in the form of a hash map. The key +// represents the name of each fruit we collect and the value represents how +// many of that particular fruit we have collected. Three types of fruits - +// Apple (4), Mango (2) and Lychee (5) are already in the basket hash map. You +// must add fruit to the basket so that there is at least one of each kind and +// more than 11 in total - we have a lot of mouths to feed. You are not allowed +// to insert any more of these fruits! + +use std::collections::HashMap; + +#[derive(Hash, PartialEq, Eq, Debug)] +enum Fruit { + Apple, + Banana, + Mango, + Lychee, + Pineapple, +} + +fn fruit_basket(basket: &mut HashMap) { + let fruit_kinds = [ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, + ]; + + for fruit in fruit_kinds { + // If fruit doesn't exist, insert it with some value. + basket.entry(fruit).or_insert(5); + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + // Don't modify this function! + fn get_fruit_basket() -> HashMap { + let content = [(Fruit::Apple, 4), (Fruit::Mango, 2), (Fruit::Lychee, 5)]; + HashMap::from_iter(content) + } + + #[test] + fn test_given_fruits_are_not_modified() { + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + assert_eq!(*basket.get(&Fruit::Apple).unwrap(), 4); + assert_eq!(*basket.get(&Fruit::Mango).unwrap(), 2); + assert_eq!(*basket.get(&Fruit::Lychee).unwrap(), 5); + } + + #[test] + fn at_least_five_types_of_fruits() { + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + let count_fruit_kinds = basket.len(); + assert!(count_fruit_kinds >= 5); + } + + #[test] + fn greater_than_eleven_fruits() { + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + let count = basket.values().sum::(); + assert!(count > 11); + } + + #[test] + fn all_fruit_types_in_basket() { + let fruit_kinds = [ + Fruit::Apple, + Fruit::Banana, + Fruit::Mango, + Fruit::Lychee, + Fruit::Pineapple, + ]; + + let mut basket = get_fruit_basket(); + fruit_basket(&mut basket); + + for fruit_kind in fruit_kinds { + let Some(amount) = basket.get(&fruit_kind) else { + panic!("Fruit kind {fruit_kind:?} was not found in basket"); + }; + assert!(*amount > 0); + } + } +} From f1bd4447924e797e8fb0012f6bc47a507438f3f5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 01:52:33 +0200 Subject: [PATCH 366/433] hashmaps3 solution --- exercises/11_hashmaps/hashmaps3.rs | 71 ++++++++++++------------- rustlings-macros/info.toml | 12 +++-- solutions/11_hashmaps/hashmaps3.rs | 84 +++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 45 deletions(-) diff --git a/exercises/11_hashmaps/hashmaps3.rs b/exercises/11_hashmaps/hashmaps3.rs index 070c3709..9f8fdd78 100644 --- a/exercises/11_hashmaps/hashmaps3.rs +++ b/exercises/11_hashmaps/hashmaps3.rs @@ -1,39 +1,38 @@ // A list of scores (one per line) of a soccer match is given. Each line is of -// the form : ",,," -// Example: England,France,4,2 (England scored 4 goals, France 2). +// the form ",,," +// Example: "England,France,4,2" (England scored 4 goals, France 2). // // You have to build a scores table containing the name of the team, the total // number of goals the team scored, and the total number of goals the team -// conceded. One approach to build the scores table is to use a Hashmap. -// The solution is partially written to use a Hashmap, -// complete it to pass the test. -// -// Make me pass the tests! +// conceded. use std::collections::HashMap; // A structure to store the goal details of a team. +#[derive(Default)] struct Team { goals_scored: u8, goals_conceded: u8, } -fn build_scores_table(results: String) -> HashMap { +fn build_scores_table(results: &str) -> HashMap<&str, Team> { // The name of the team is the key and its associated struct is the value. - let mut scores: HashMap = HashMap::new(); + let mut scores = HashMap::new(); - for r in results.lines() { - let v: Vec<&str> = r.split(',').collect(); - let team_1_name = v[0].to_string(); - let team_1_score: u8 = v[2].parse().unwrap(); - let team_2_name = v[1].to_string(); - let team_2_score: u8 = v[3].parse().unwrap(); - // TODO: Populate the scores table with details extracted from the - // current line. Keep in mind that goals scored by team_1 - // will be the number of goals conceded by team_2, and similarly - // goals scored by team_2 will be the number of goals conceded by - // team_1. + for line in results.lines() { + let mut split_iterator = line.split(','); + // NOTE: We use `unwrap` because we didn't deal with error handling yet. + let team_1_name = split_iterator.next().unwrap(); + let team_2_name = split_iterator.next().unwrap(); + let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + + // TODO: Populate the scores table with the extracted details. + // Keep in mind that goals scored by team 1 will be the number of goals + // conceded by team 2. Similarly, goals scored by team 2 will be the + // number of goals conceded by team 1. } + scores } @@ -45,40 +44,34 @@ fn main() { mod tests { use super::*; - fn get_results() -> String { - let results = "".to_string() - + "England,France,4,2\n" - + "France,Italy,3,1\n" - + "Poland,Spain,2,0\n" - + "Germany,England,2,1\n"; - results - } + const RESULTS: &str = "England,France,4,2 +France,Italy,3,1 +Poland,Spain,2,0 +Germany,England,2,1 +England,Spain,1,0"; #[test] fn build_scores() { - let scores = build_scores_table(get_results()); + let scores = build_scores_table(RESULTS); - let mut keys: Vec<&String> = scores.keys().collect(); - keys.sort(); - assert_eq!( - keys, - vec!["England", "France", "Germany", "Italy", "Poland", "Spain"] - ); + assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"] + .into_iter() + .all(|team_name| scores.contains_key(team_name))); } #[test] fn validate_team_score_1() { - let scores = build_scores_table(get_results()); + let scores = build_scores_table(RESULTS); let team = scores.get("England").unwrap(); - assert_eq!(team.goals_scored, 5); + assert_eq!(team.goals_scored, 6); assert_eq!(team.goals_conceded, 4); } #[test] fn validate_team_score_2() { - let scores = build_scores_table(get_results()); + let scores = build_scores_table(RESULTS); let team = scores.get("Spain").unwrap(); assert_eq!(team.goals_scored, 0); - assert_eq!(team.goals_conceded, 2); + assert_eq!(team.goals_conceded, 3); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 0da573be..c5ce8479 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -574,16 +574,18 @@ https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-va name = "hashmaps3" dir = "11_hashmaps" hint = """ -Hint 1: Use the `entry()` and `or_insert()` methods of `HashMap` to insert - entries corresponding to each team in the scores table. +Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of + `HashMap` to insert the default value of `Team` if a team doesn't + exist in the table yet. -Learn more at https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value +Learn more in The Book: +https://doc.rust-lang.org/stable/book/ch08-03-hash-maps.html#only-inserting-a-value-if-the-key-has-no-value Hint 2: If there is already an entry for a given key, the value returned by `entry()` can be updated based on the existing value. -Learn more at https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-value-based-on-the-old-value -""" +Learn more in The Book: +https://doc.rust-lang.org/book/ch08-03-hash-maps.html#updating-a-value-based-on-the-old-value""" # QUIZ 2 diff --git a/solutions/11_hashmaps/hashmaps3.rs b/solutions/11_hashmaps/hashmaps3.rs index 4e181989..f4059bbe 100644 --- a/solutions/11_hashmaps/hashmaps3.rs +++ b/solutions/11_hashmaps/hashmaps3.rs @@ -1 +1,83 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// A list of scores (one per line) of a soccer match is given. Each line is of +// the form ",,," +// Example: "England,France,4,2" (England scored 4 goals, France 2). +// +// You have to build a scores table containing the name of the team, the total +// number of goals the team scored, and the total number of goals the team +// conceded. + +use std::collections::HashMap; + +// A structure to store the goal details of a team. +#[derive(Default)] +struct Team { + goals_scored: u8, + goals_conceded: u8, +} + +fn build_scores_table(results: &str) -> HashMap<&str, Team> { + // The name of the team is the key and its associated struct is the value. + let mut scores = HashMap::new(); + + for line in results.lines() { + let mut split_iterator = line.split(','); + // NOTE: We use `unwrap` because we didn't deal with error handling yet. + let team_1_name = split_iterator.next().unwrap(); + let team_2_name = split_iterator.next().unwrap(); + let team_1_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); + + // Insert the default with zeros if a team doesn't exist yet. + let mut team_1 = scores.entry(team_1_name).or_insert_with(|| Team::default()); + // Update the values. + team_1.goals_scored += team_1_score; + team_1.goals_conceded += team_2_score; + + // Similarely for the second team. + let mut team_2 = scores.entry(team_2_name).or_insert_with(|| Team::default()); + team_2.goals_scored += team_2_score; + team_2.goals_conceded += team_1_score; + } + + scores +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + const RESULTS: &str = "England,France,4,2 +France,Italy,3,1 +Poland,Spain,2,0 +Germany,England,2,1 +England,Spain,1,0"; + + #[test] + fn build_scores() { + let scores = build_scores_table(RESULTS); + + assert!(["England", "France", "Germany", "Italy", "Poland", "Spain"] + .into_iter() + .all(|team_name| scores.contains_key(team_name))); + } + + #[test] + fn validate_team_score_1() { + let scores = build_scores_table(RESULTS); + let team = scores.get("England").unwrap(); + assert_eq!(team.goals_scored, 6); + assert_eq!(team.goals_conceded, 4); + } + + #[test] + fn validate_team_score_2() { + let scores = build_scores_table(RESULTS); + let team = scores.get("Spain").unwrap(); + assert_eq!(team.goals_scored, 0); + assert_eq!(team.goals_conceded, 3); + } +} From 29bcb282dacead96df6e6cdbec9ac1ba8008d90f Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 02:25:59 +0200 Subject: [PATCH 367/433] quiz2 solution --- exercises/quizzes/quiz2.rs | 45 ++++++++-------- solutions/quizzes/quiz2.rs | 108 ++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 23 deletions(-) diff --git a/exercises/quizzes/quiz2.rs b/exercises/quizzes/quiz2.rs index e01e3f1d..fd6bc773 100644 --- a/exercises/quizzes/quiz2.rs +++ b/exercises/quizzes/quiz2.rs @@ -11,10 +11,11 @@ // - Uppercase the string // - Trim the string // - Append "bar" to the string a specified amount of times +// // The exact form of this will be: -// - The input is going to be a Vector of a 2-length tuple, +// - The input is going to be a vector of a 2-length tuple, // the first element is the string, the second one is the command. -// - The output element is going to be a Vector of strings. +// - The output element is going to be a vector of strings. enum Command { Uppercase, @@ -25,15 +26,8 @@ enum Command { mod my_module { use super::Command; - // TODO: Complete the function signature! - pub fn transformer(input: ???) -> ??? { - // TODO: Complete the output declaration! - let mut output: ??? = vec![]; - for (string, command) in input.iter() { - // TODO: Complete the function body. You can do it! - } - output - } + // TODO: Complete the function. + // pub fn transformer(input: ???) -> ??? { ??? } } fn main() { @@ -43,20 +37,27 @@ fn main() { #[cfg(test)] mod tests { // TODO: What do we need to import to have `transformer` in scope? - use ???; + // use ???; use super::Command; #[test] fn it_works() { - let output = transformer(vec![ - ("hello".into(), Command::Uppercase), - (" all roads lead to rome! ".into(), Command::Trim), - ("foo".into(), Command::Append(1)), - ("bar".into(), Command::Append(5)), - ]); - assert_eq!(output[0], "HELLO"); - assert_eq!(output[1], "all roads lead to rome!"); - assert_eq!(output[2], "foobar"); - assert_eq!(output[3], "barbarbarbarbarbar"); + let input = vec![ + ("hello".to_string(), Command::Uppercase), + (" all roads lead to rome! ".to_string(), Command::Trim), + ("foo".to_string(), Command::Append(1)), + ("bar".to_string(), Command::Append(5)), + ]; + let output = transformer(input); + + assert_eq!( + output, + [ + "HELLO", + "all roads lead to rome!", + "foobar", + "barbarbarbarbarbar", + ] + ); } } diff --git a/solutions/quizzes/quiz2.rs b/solutions/quizzes/quiz2.rs index 4e181989..0d2a5132 100644 --- a/solutions/quizzes/quiz2.rs +++ b/solutions/quizzes/quiz2.rs @@ -1 +1,107 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This is a quiz for the following sections: +// - Strings +// - Vecs +// - Move semantics +// - Modules +// - Enums +// +// Let's build a little machine in the form of a function. As input, we're going +// to give a list of strings and commands. These commands determine what action +// is going to be applied to the string. It can either be: +// - Uppercase the string +// - Trim the string +// - Append "bar" to the string a specified amount of times +// +// The exact form of this will be: +// - The input is going to be a vector of a 2-length tuple, +// the first element is the string, the second one is the command. +// - The output element is going to be a vector of strings. + +enum Command { + Uppercase, + Trim, + Append(usize), +} + +mod my_module { + use super::Command; + + // The solution with a loop. Check out `transformer_iter` for a version + // with iterators. + pub fn transformer(input: Vec<(String, Command)>) -> Vec { + let mut output = Vec::new(); + + for (mut string, command) in input { + // Create the new string. + let new_string = match command { + Command::Uppercase => string.to_uppercase(), + Command::Trim => string.trim().to_string(), + Command::Append(n) => { + for _ in 0..n { + string += "bar"; + } + string + } + }; + + // Push the new string to the output vector. + output.push(new_string); + } + + output + } + + // Equivalent to `transform` but uses an iterator instead of a loop for + // comparison. Don't worry, we will practice iterators later ;) + pub fn transformer_iter(input: Vec<(String, Command)>) -> Vec { + input + .into_iter() + .map(|(mut string, command)| match command { + Command::Uppercase => string.to_uppercase(), + Command::Trim => string.trim().to_string(), + Command::Append(n) => { + for _ in 0..n { + string += "bar"; + } + string + } + }) + .collect() + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // Import `transformer`. + use super::my_module::transformer; + + use super::my_module::transformer_iter; + use super::Command; + + #[test] + fn it_works() { + for transformer in [transformer, transformer_iter] { + let input = vec![ + ("hello".to_string(), Command::Uppercase), + (" all roads lead to rome! ".to_string(), Command::Trim), + ("foo".to_string(), Command::Append(1)), + ("bar".to_string(), Command::Append(5)), + ]; + let output = transformer(input); + + assert_eq!( + output, + [ + "HELLO", + "all roads lead to rome!", + "foobar", + "barbarbarbarbarbar", + ] + ); + } + } +} From 1694682aa4ee848033169449a3f64d2e163f5638 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 02:26:04 +0200 Subject: [PATCH 368/433] Fix typos --- exercises/07_structs/structs3.rs | 3 ++- exercises/10_modules/modules2.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/exercises/07_structs/structs3.rs b/exercises/07_structs/structs3.rs index 18a6cc98..93b57fef 100644 --- a/exercises/07_structs/structs3.rs +++ b/exercises/07_structs/structs3.rs @@ -25,7 +25,8 @@ impl Package { // TODO: Add the correct return type to the function signature. fn is_international(&self) { - // TODO: Read the tests that use this method to find out when a package is concidered international. + // TODO: Read the tests that use this method to find out when a package + // is considered international. } // TODO: Add the correct return type to the function signature. diff --git a/exercises/10_modules/modules2.rs b/exercises/10_modules/modules2.rs index 24dce41f..782a70ea 100644 --- a/exercises/10_modules/modules2.rs +++ b/exercises/10_modules/modules2.rs @@ -2,7 +2,7 @@ // the `use` and `as` keywords. mod delicious_snacks { - // TODO: Add the follwing two `use` statements after fixing them. + // TODO: Add the following two `use` statements after fixing them. // use self::fruits::PEAR as ???; // use self::veggies::CUCUMBER as ???; From c31e15c4cf5085adcf544a33ac256364fc2bcfbf Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 12:59:10 +0200 Subject: [PATCH 369/433] options1 solution --- exercises/12_options/options1.rs | 30 ++++++++++++------------ rustlings-macros/info.toml | 2 +- solutions/12_options/options1.rs | 39 +++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/exercises/12_options/options1.rs b/exercises/12_options/options1.rs index b7cf7b0b..5009f8b6 100644 --- a/exercises/12_options/options1.rs +++ b/exercises/12_options/options1.rs @@ -1,12 +1,9 @@ // This function returns how much icecream there is left in the fridge. -// If it's before 10PM, there's 5 scoops left. At 10PM, someone eats it -// all, so there'll be no more left :( -fn maybe_icecream(time_of_day: u16) -> Option { - // We use the 24-hour system here, so 10PM is a value of 22 and 12AM is a - // value of 0. The Option output should gracefully handle cases where - // time_of_day > 23. - // TODO: Complete the function body - remember to return an Option! - ??? +// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, +// someone eats it all, so no icecream is left (value 0). Return `None` if +// `hour_of_day` is higher than 23. +fn maybe_icecream(hour_of_day: u16) -> Option { + // TODO: Complete the function body. } fn main() { @@ -17,6 +14,14 @@ fn main() { mod tests { use super::*; + #[test] + fn raw_value() { + // TODO: Fix this test. How do you get the value contained in the + // Option? + let icecreams = maybe_icecream(12); + assert_eq!(icecreams, 5); + } + #[test] fn check_icecream() { assert_eq!(maybe_icecream(0), Some(5)); @@ -24,14 +29,7 @@ mod tests { assert_eq!(maybe_icecream(18), Some(5)); assert_eq!(maybe_icecream(22), Some(0)); assert_eq!(maybe_icecream(23), Some(0)); + assert_eq!(maybe_icecream(24), None); assert_eq!(maybe_icecream(25), None); } - - #[test] - fn raw_value() { - // TODO: Fix this test. How do you get at the value contained in the - // Option? - let icecreams = maybe_icecream(12); - assert_eq!(icecreams, 5); - } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index c5ce8479..3694b94f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -603,7 +603,7 @@ hint = """ Options can have a `Some` value, with an inner value, or a `None` value, without an inner value. -There's multiple ways to get at the inner value, you can use `unwrap`, or +There are multiple ways to get at the inner value, you can use `unwrap`, or pattern match. Unwrapping is the easiest, but how do you do it safely so that it doesn't panic in your face later?""" diff --git a/solutions/12_options/options1.rs b/solutions/12_options/options1.rs index 4e181989..1ffbb045 100644 --- a/solutions/12_options/options1.rs +++ b/solutions/12_options/options1.rs @@ -1 +1,38 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This function returns how much icecream there is left in the fridge. +// If it's before 22:00 (24-hour system), then 5 scoops are left. At 22:00, +// someone eats it all, so no icecream is left (value 0). Return `None` if +// `hour_of_day` is higher than 23. +fn maybe_icecream(hour_of_day: u16) -> Option { + match hour_of_day { + 0..22 => Some(5), + 22..24 => Some(0), + _ => None, + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn raw_value() { + // Using `unwrap` is fine in a test. + let icecreams = maybe_icecream(12).unwrap(); + assert_eq!(icecreams, 5); + } + + #[test] + fn check_icecream() { + assert_eq!(maybe_icecream(0), Some(5)); + assert_eq!(maybe_icecream(9), Some(5)); + assert_eq!(maybe_icecream(18), Some(5)); + assert_eq!(maybe_icecream(22), Some(0)); + assert_eq!(maybe_icecream(23), Some(0)); + assert_eq!(maybe_icecream(24), None); + assert_eq!(maybe_icecream(25), None); + } +} From a91888e79e69e04e57c2049cdf940a70201e1d6e Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 14:35:05 +0200 Subject: [PATCH 370/433] option2 solution --- exercises/12_options/options2.rs | 10 ++++----- rustlings-macros/info.toml | 4 ++-- solutions/12_options/options2.rs | 38 +++++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/exercises/12_options/options2.rs b/exercises/12_options/options2.rs index 01f84c58..07c27c6e 100644 --- a/exercises/12_options/options2.rs +++ b/exercises/12_options/options2.rs @@ -9,7 +9,7 @@ mod tests { let target = "rustlings"; let optional_target = Some(target); - // TODO: Make this an if let statement whose value is "Some" type + // TODO: Make this an if-let statement whose value is `Some`. word = optional_target { assert_eq!(word, target); } @@ -20,15 +20,15 @@ mod tests { let range = 10; let mut optional_integers: Vec> = vec![None]; - for i in 1..(range + 1) { + for i in 1..=range { optional_integers.push(Some(i)); } let mut cursor = range; - // TODO: make this a while let statement - remember that vector.pop also - // adds another layer of Option. You can stack `Option`s into - // while let and if let. + // TODO: Make this a while-let statement. Remember that `Vec::pop()` + // adds another layer of `Option`. You can do nested pattern matching + // in if-let and while-let statements. integer = optional_integers.pop() { assert_eq!(integer, cursor); cursor -= 1; diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 3694b94f..6027b6b1 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -616,9 +616,9 @@ Check out: - https://doc.rust-lang.org/rust-by-example/flow_control/if_let.html - https://doc.rust-lang.org/rust-by-example/flow_control/while_let.html -Remember that `Option`s can be stacked in `if let` and `while let`. +Remember that `Option`s can be nested in if-let and while-let statements. -For example: `Some(Some(variable)) = variable2` +For example: `if let Some(Some(x)) = y` Also see `Option::flatten` """ diff --git a/solutions/12_options/options2.rs b/solutions/12_options/options2.rs index 4e181989..0f24665f 100644 --- a/solutions/12_options/options2.rs +++ b/solutions/12_options/options2.rs @@ -1 +1,37 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn simple_option() { + let target = "rustlings"; + let optional_target = Some(target); + + // if-let + if let Some(word) = optional_target { + assert_eq!(word, target); + } + } + + #[test] + fn layered_option() { + let range = 10; + let mut optional_integers: Vec> = vec![None]; + + for i in 1..=range { + optional_integers.push(Some(i)); + } + + let mut cursor = range; + + // while-let with nested pattern matching + while let Some(Some(integer)) = optional_integers.pop() { + assert_eq!(integer, cursor); + cursor -= 1; + } + + assert_eq!(cursor, 0); + } +} From 25b5686dd2ab2e3d5a228a71e9631c50ea50fffe Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 14:47:57 +0200 Subject: [PATCH 371/433] options3 solution --- exercises/12_options/options3.rs | 13 ++++++++----- rustlings-macros/info.toml | 3 ++- solutions/12_options/options3.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/exercises/12_options/options3.rs b/exercises/12_options/options3.rs index 5b70a792..4cedb512 100644 --- a/exercises/12_options/options3.rs +++ b/exercises/12_options/options3.rs @@ -1,14 +1,17 @@ +#[derive(Debug)] struct Point { x: i32, y: i32, } fn main() { - let y: Option = Some(Point { x: 100, y: 200 }); + let optional_point = Some(Point { x: 100, y: 200 }); - match y { - Some(p) => println!("Co-ordinates are {},{} ", p.x, p.y), - _ => panic!("no match!"), + // TODO: Fix the compiler error by adding something to this match statement. + match optional_point { + Some(p) => println!("Co-ordinates are {},{}", p.x, p.y), + _ => panic!("No match!"), } - y; // Fix without deleting this line. + + println!("{optional_point:?}"); // Don't change this line. } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 6027b6b1..5a47c85f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -631,7 +631,8 @@ hint = """ The compiler says a partial move happened in the `match` statement. How can this be avoided? The compiler shows the correction needed. -After making the correction as suggested by the compiler, do read: +After making the correction as suggested by the compiler, read the related docs +page: https://doc.rust-lang.org/std/keyword.ref.html""" # ERROR HANDLING diff --git a/solutions/12_options/options3.rs b/solutions/12_options/options3.rs index 4e181989..0081eeb2 100644 --- a/solutions/12_options/options3.rs +++ b/solutions/12_options/options3.rs @@ -1 +1,26 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(Debug)] +struct Point { + x: i32, + y: i32, +} + +fn main() { + let optional_point = Some(Point { x: 100, y: 200 }); + + // Solution 1: Matching over the `Option` (not `&Option`) but without moving + // out of the `Some` variant. + match optional_point { + Some(ref p) => println!("Co-ordinates are {},{}", p.x, p.y), + // ^^^ added + _ => panic!("No match!"), + } + + // Solution 2: Matching over a reference (`&Option`) by added `&` before + // `optional_point`. + match &optional_point { + Some(p) => println!("Co-ordinates are {},{}", p.x, p.y), + _ => panic!("No match!"), + } + + println!("{optional_point:?}"); +} From 097f3c74ea16bad95a659fc41a494f24e07656d1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 15:06:29 +0200 Subject: [PATCH 372/433] errors1 solution --- exercises/13_error_handling/errors1.rs | 30 ++++++++++----------- rustlings-macros/info.toml | 4 +-- solutions/13_error_handling/errors1.rs | 36 +++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index e3e04823..6d9701b1 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -1,22 +1,22 @@ -// This function refuses to generate text to be printed on a nametag if you pass -// it an empty string. It'd be nicer if it explained what the problem was, -// instead of just sometimes returning `None`. Thankfully, Rust has a similar -// construct to `Option` that can be used to express error conditions. Let's use -// it! - -fn main() { - // You can optionally experiment here. -} - +// TODO: This function refuses to generate text to be printed on a nametag if +// you pass it an empty string. It'd be nicer if it explained what the problem +// was instead of just returning `None`. Thankfully, Rust has a similar +// construct to `Option` that can be used to express error conditions. Change +// the function signature and body to return `Result` instead +// of `Option`. fn generate_nametag_text(name: String) -> Option { if name.is_empty() { // Empty names aren't allowed. None } else { - Some(format!("Hi! My name is {}", name)) + Some(format!("Hi! My name is {name}")) } } +fn main() { + // You can optionally experiment here. +} + #[cfg(test)] mod tests { use super::*; @@ -24,17 +24,17 @@ mod tests { #[test] fn generates_nametag_text_for_a_nonempty_name() { assert_eq!( - generate_nametag_text("Beyoncé".into()), - Ok("Hi! My name is Beyoncé".into()) + generate_nametag_text("Beyoncé".to_string()).as_deref(), + Ok("Hi! My name is Beyoncé"), ); } #[test] fn explains_why_generating_nametag_text_fails() { assert_eq!( - generate_nametag_text("".into()), + generate_nametag_text(String::new()).as_deref(), // Don't change this line - Err("`name` was empty; it must be nonempty.".into()) + Err("`name` was empty; it must be nonempty."), ); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 5a47c85f..3d8da58f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -647,8 +647,8 @@ is that `generate_nametag_text` should return a `Result` instead of an `Option`. To make this change, you'll need to: - update the return type in the function signature to be a `Result` that could be the variants `Ok(String)` and `Err(String)` - - change the body of the function to return `Ok(stuff)` where it currently - returns `Some(stuff)` + - change the body of the function to return `Ok(…)` where it currently + returns `Some(…)` - change the body of the function to return `Err(error message)` where it currently returns `None`""" diff --git a/solutions/13_error_handling/errors1.rs b/solutions/13_error_handling/errors1.rs index 4e181989..2a13bfdd 100644 --- a/solutions/13_error_handling/errors1.rs +++ b/solutions/13_error_handling/errors1.rs @@ -1 +1,35 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn generate_nametag_text(name: String) -> Result { + // ^^^^^^ ^^^^^^ + if name.is_empty() { + // `Err(String)` instead of `None`. + Err("Empty names aren't allowed".to_string()) + } else { + // `Ok` instead of `Some`. + Ok(format!("Hi! My name is {name}")) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generates_nametag_text_for_a_nonempty_name() { + assert_eq!( + generate_nametag_text("Beyoncé".to_string()).as_deref(), + Ok("Hi! My name is Beyoncé"), + ); + } + + #[test] + fn explains_why_generating_nametag_text_fails() { + assert_eq!( + generate_nametag_text(String::new()).as_deref(), + Err("`name` was empty; it must be nonempty."), + ); + } +} From 2afe6b38d3fe8d851b0d37f85c0d058388603127 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 15:12:58 +0200 Subject: [PATCH 373/433] Fix tests --- exercises/13_error_handling/errors1.rs | 7 ++++--- solutions/13_error_handling/errors1.rs | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/exercises/13_error_handling/errors1.rs b/exercises/13_error_handling/errors1.rs index 6d9701b1..ec7cb3cb 100644 --- a/exercises/13_error_handling/errors1.rs +++ b/exercises/13_error_handling/errors1.rs @@ -32,9 +32,10 @@ mod tests { #[test] fn explains_why_generating_nametag_text_fails() { assert_eq!( - generate_nametag_text(String::new()).as_deref(), - // Don't change this line - Err("`name` was empty; it must be nonempty."), + generate_nametag_text(String::new()) + .as_ref() + .map_err(|e| e.as_str()), + Err("Empty names aren't allowed"), ); } } diff --git a/solutions/13_error_handling/errors1.rs b/solutions/13_error_handling/errors1.rs index 2a13bfdd..f552ca7f 100644 --- a/solutions/13_error_handling/errors1.rs +++ b/solutions/13_error_handling/errors1.rs @@ -28,8 +28,10 @@ mod tests { #[test] fn explains_why_generating_nametag_text_fails() { assert_eq!( - generate_nametag_text(String::new()).as_deref(), - Err("`name` was empty; it must be nonempty."), + generate_nametag_text(String::new()) + .as_ref() + .map_err(|e| e.as_str()), + Err("Empty names aren't allowed"), ); } } From 050a23ce6763fedf0906cd1c04b76888aae12f7d Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 15:36:14 +0200 Subject: [PATCH 374/433] errors2 solution --- exercises/13_error_handling/errors2.rs | 19 +++++---- rustlings-macros/info.toml | 7 ++-- solutions/13_error_handling/errors2.rs | 58 +++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/exercises/13_error_handling/errors2.rs b/exercises/13_error_handling/errors2.rs index 345a0eef..e50a9299 100644 --- a/exercises/13_error_handling/errors2.rs +++ b/exercises/13_error_handling/errors2.rs @@ -2,16 +2,16 @@ // 5 tokens, and whenever you purchase items there is a processing fee of 1 // token. A player of the game will type in how many items they want to buy, and // the `total_cost` function will calculate the total cost of the items. Since -// the player typed in the quantity, though, we get it as a string-- and they -// might have typed anything, not just numbers! +// the player typed in the quantity, we get it as a string. They might have +// typed anything, not just numbers! // // Right now, this function isn't handling the error case at all (and isn't -// handling the success case properly either). What we want to do is: if we call +// handling the success case properly either). What we want to do is: If we call // the `total_cost` function on a string that is not a number, that function -// will return a `ParseIntError`, and in that case, we want to immediately -// return that error from our function and not try to multiply and add. +// will return a `ParseIntError`. In that case, we want to immediately return +// that error from our function and not try to multiply and add. // -// There are at least two ways to implement this that are both correct-- but one +// There are at least two ways to implement this that are both correct. But one // is a lot shorter! use std::num::ParseIntError; @@ -19,6 +19,8 @@ use std::num::ParseIntError; fn total_cost(item_quantity: &str) -> Result { let processing_fee = 1; let cost_per_item = 5; + + // TODO: Handle the error case as described above. let qty = item_quantity.parse::(); Ok(qty * cost_per_item + processing_fee) @@ -31,6 +33,7 @@ fn main() { #[cfg(test)] mod tests { use super::*; + use std::num::IntErrorKind; #[test] fn item_quantity_is_a_valid_number() { @@ -40,8 +43,8 @@ mod tests { #[test] fn item_quantity_is_an_invalid_number() { assert_eq!( - total_cost("beep boop").unwrap_err().to_string(), - "invalid digit found in string" + total_cost("beep boop").unwrap_err().kind(), + &IntErrorKind::InvalidDigit, ); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 3d8da58f..2a4a24ea 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -660,12 +660,11 @@ One way to handle this is using a `match` statement on `item_quantity.parse::()` where the cases are `Ok(something)` and `Err(something)`. -This pattern is very common in Rust, though, so there's a `?` operator that +This pattern is very common in Rust, though, so there's the `?` operator that does pretty much what you would make that match statement do for you! -Take a look at this section of the 'Error Handling' chapter: -https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator -and give it a try!""" +Take a look at this section of the "Error Handling" chapter: +https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator""" [[exercises]] name = "errors3" diff --git a/solutions/13_error_handling/errors2.rs b/solutions/13_error_handling/errors2.rs index 4e181989..de7c32b5 100644 --- a/solutions/13_error_handling/errors2.rs +++ b/solutions/13_error_handling/errors2.rs @@ -1 +1,57 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Say we're writing a game where you can buy items with tokens. All items cost +// 5 tokens, and whenever you purchase items there is a processing fee of 1 +// token. A player of the game will type in how many items they want to buy, and +// the `total_cost` function will calculate the total cost of the items. Since +// the player typed in the quantity, we get it as a string. They might have +// typed anything, not just numbers! +// +// Right now, this function isn't handling the error case at all (and isn't +// handling the success case properly either). What we want to do is: If we call +// the `total_cost` function on a string that is not a number, that function +// will return a `ParseIntError`. In that case, we want to immediately return +// that error from our function and not try to multiply and add. +// +// There are at least two ways to implement this that are both correct. But one +// is a lot shorter! + +use std::num::ParseIntError; + +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + + // Added `?` to propagate the error. + let qty = item_quantity.parse::()?; + // ^ added + + // Equivalent to this verbose version: + let qty = match item_quantity.parse::() { + Ok(v) => v, + Err(e) => return Err(e), + }; + + Ok(qty * cost_per_item + processing_fee) +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + use std::num::IntErrorKind; + + #[test] + fn item_quantity_is_a_valid_number() { + assert_eq!(total_cost("34"), Ok(171)); + } + + #[test] + fn item_quantity_is_an_invalid_number() { + assert_eq!( + total_cost("beep boop").unwrap_err().kind(), + &IntErrorKind::InvalidDigit, + ); + } +} From c46d8bdf95c9a2025ee943feb208102a94b25ee6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 15:44:33 +0200 Subject: [PATCH 375/433] errors3 solution --- exercises/13_error_handling/errors3.rs | 21 +++++++++------- rustlings-macros/info.toml | 4 ++-- solutions/13_error_handling/errors3.rs | 33 +++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/exercises/13_error_handling/errors3.rs b/exercises/13_error_handling/errors3.rs index 2ef84f98..33a7b877 100644 --- a/exercises/13_error_handling/errors3.rs +++ b/exercises/13_error_handling/errors3.rs @@ -4,6 +4,17 @@ use std::num::ParseIntError; +// Don't change this function. +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + let qty = item_quantity.parse::()?; + + Ok(qty * cost_per_item + processing_fee) +} + +// TODO: Fix the compiler error by changing the signature and body of the +// `main` function. fn main() { let mut tokens = 100; let pretend_user_input = "8"; @@ -14,14 +25,6 @@ fn main() { println!("You can't afford that many!"); } else { tokens -= cost; - println!("You now have {} tokens.", tokens); + println!("You now have {tokens} tokens."); } } - -fn total_cost(item_quantity: &str) -> Result { - let processing_fee = 1; - let cost_per_item = 5; - let qty = item_quantity.parse::()?; - - Ok(qty * cost_per_item + processing_fee) -} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 2a4a24ea..74cb79dd 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -675,8 +675,8 @@ If other functions can return a `Result`, why shouldn't `main`? It's a fairly common convention to return something like `Result<(), ErrorType>` from your `main` function. -The unit (`()`) type is there because nothing is really needed in terms of -positive results.""" +The unit type `()` is there because nothing is really needed in terms of a +positive result.""" [[exercises]] name = "errors4" diff --git a/solutions/13_error_handling/errors3.rs b/solutions/13_error_handling/errors3.rs index 4e181989..63f4aba1 100644 --- a/solutions/13_error_handling/errors3.rs +++ b/solutions/13_error_handling/errors3.rs @@ -1 +1,32 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This is a program that is trying to use a completed version of the +// `total_cost` function from the previous exercise. It's not working though! +// Why not? What should we do to fix it? + +use std::num::ParseIntError; + +// Don't change this function. +fn total_cost(item_quantity: &str) -> Result { + let processing_fee = 1; + let cost_per_item = 5; + let qty = item_quantity.parse::()?; + + Ok(qty * cost_per_item + processing_fee) +} + +fn main() -> Result<(), ParseIntError> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ added + let mut tokens = 100; + let pretend_user_input = "8"; + + let cost = total_cost(pretend_user_input)?; + + if cost > tokens { + println!("You can't afford that many!"); + } else { + tokens -= cost; + println!("You now have {tokens} tokens."); + } + + // Added this line to return the `Ok` variant of the expected `Result`. + Ok(()) +} From 9b7a5c041e9856379154b109b2ee2f3e979d70f7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 15:54:18 +0200 Subject: [PATCH 376/433] errors4 solution --- exercises/13_error_handling/errors4.rs | 23 ++++++++------ rustlings-macros/info.toml | 8 ++--- solutions/13_error_handling/errors4.rs | 43 +++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/exercises/13_error_handling/errors4.rs b/exercises/13_error_handling/errors4.rs index 993d42a1..ba01e54b 100644 --- a/exercises/13_error_handling/errors4.rs +++ b/exercises/13_error_handling/errors4.rs @@ -1,16 +1,16 @@ -#[derive(PartialEq, Debug)] -struct PositiveNonzeroInteger(u64); - #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + impl PositiveNonzeroInteger { - fn new(value: i64) -> Result { - // Hmm... Why is this always returning an Ok value? - Ok(PositiveNonzeroInteger(value as u64)) + fn new(value: i64) -> Result { + // TODO: This function shouldn't always return an `Ok`. + Ok(Self(value as u64)) } } @@ -24,11 +24,14 @@ mod tests { #[test] fn test_creation() { - assert!(PositiveNonzeroInteger::new(10).is_ok()); assert_eq!( - Err(CreationError::Negative), - PositiveNonzeroInteger::new(-10) + PositiveNonzeroInteger::new(10), + Ok(PositiveNonzeroInteger(10)), ); - assert_eq!(Err(CreationError::Zero), PositiveNonzeroInteger::new(0)); + assert_eq!( + PositiveNonzeroInteger::new(-10), + Err(CreationError::Negative), + ); + assert_eq!(PositiveNonzeroInteger::new(0), Err(CreationError::Zero)); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 74cb79dd..d39044c0 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -683,11 +683,9 @@ name = "errors4" dir = "13_error_handling" hint = """ `PositiveNonzeroInteger::new` is always creating a new instance and returning -an `Ok` result. - -It should be doing some checking, returning an `Err` result if those checks -fail, and only returning an `Ok` result if those checks determine that -everything is... okay :)""" +an `Ok` result. But it should be doing some checking, returning an `Err` if +those checks fail, and only returning an `Ok` if those checks determine that +everything is… okay :)""" [[exercises]] name = "errors5" diff --git a/solutions/13_error_handling/errors4.rs b/solutions/13_error_handling/errors4.rs index 4e181989..c43f493b 100644 --- a/solutions/13_error_handling/errors4.rs +++ b/solutions/13_error_handling/errors4.rs @@ -1 +1,42 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + if value == 0 { + Err(CreationError::Zero) + } else if value < 0 { + Err(CreationError::Negative) + } else { + Ok(Self(value as u64)) + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_creation() { + assert_eq!( + PositiveNonzeroInteger::new(10), + Ok(PositiveNonzeroInteger(10)), + ); + assert_eq!( + PositiveNonzeroInteger::new(-10), + Err(CreationError::Negative), + ); + assert_eq!(PositiveNonzeroInteger::new(0), Err(CreationError::Zero)); + } +} From 720b280bc1d8230821b4e6f2466eddb43245b8ff Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 16:59:13 +0200 Subject: [PATCH 377/433] Update deps --- Cargo.lock | 50 ++++++++++++++++++++++++++++++-------------------- Cargo.toml | 4 ++-- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82c06f9b..f41ba86e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,9 +113,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" @@ -229,7 +229,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crossterm_winapi", "libc", "mio", @@ -262,9 +262,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" @@ -363,6 +363,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -450,7 +459,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crossbeam-channel", "filetime", "fsevent-sys", @@ -559,9 +568,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -577,19 +586,20 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.26.3" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cassowary", "compact_str", "crossterm", - "itertools", + "itertools 0.13.0", "lru", "paste", "stability", "strum", + "strum_macros", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -610,7 +620,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -719,9 +729,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" dependencies = [ "itoa", "ryu", @@ -797,9 +807,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] @@ -819,9 +829,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -874,7 +884,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" dependencies = [ - "itertools", + "itertools 0.12.1", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index 8e67312d..64844c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,9 +52,9 @@ crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.0" -ratatui = { version = "0.26.3", default-features = false, features = ["crossterm"] } +ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" } -serde_json = "1.0.117" +serde_json = "1.0.118" serde.workspace = true toml_edit.workspace = true From 129884aff74964d13aba8309014554b5625d6e5b Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 26 Jun 2024 18:21:19 +0200 Subject: [PATCH 378/433] errors5 solution --- exercises/13_error_handling/errors5.rs | 76 ++++++++++++-------------- rustlings-macros/info.toml | 17 +++--- solutions/13_error_handling/errors5.rs | 55 ++++++++++++++++++- 3 files changed, 96 insertions(+), 52 deletions(-) diff --git a/exercises/13_error_handling/errors5.rs b/exercises/13_error_handling/errors5.rs index 71925626..d0044db2 100644 --- a/exercises/13_error_handling/errors5.rs +++ b/exercises/13_error_handling/errors5.rs @@ -1,38 +1,18 @@ -// This program uses an altered version of the code from errors4. -// -// This exercise uses some concepts that we won't get to until later in the -// course, like `Box` and the `From` trait. It's not important to understand -// them in detail right now, but you can read ahead if you like. For now, think -// of the `Box` type as an "I want anything that does ???" type, which, -// given Rust's usual standards for runtime safety, should strike you as -// somewhat lenient! +// This exercise is an altered version of the `errors4` exercise. It uses some +// concepts that we won't get to until later in the course, like `Box` and the +// `From` trait. It's not important to understand them in detail right now, but +// you can read ahead if you like. For now, think of the `Box` type as +// an "I want anything that does ???" type. // // In short, this particular use case for boxes is for when you want to own a // value and you care only that it is a type which implements a particular -// trait. To do so, The Box is declared as of type Box where Trait is -// the trait the compiler looks for on any value used in that context. For this -// exercise, that context is the potential errors which can be returned in a -// Result. -// -// What can we use to describe both errors? In other words, is there a trait -// which both errors implement? +// trait. To do so, The `Box` is declared as of type `Box` where +// `Trait` is the trait the compiler looks for on any value used in that +// context. For this exercise, that context is the potential errors which +// can be returned in a `Result`. -use std::error; +use std::error::Error; use std::fmt; -use std::num::ParseIntError; - -// TODO: update the return type of `main()` to make this compile. -fn main() -> Result<(), Box> { - let pretend_user_input = "42"; - let x: i64 = pretend_user_input.parse()?; - println!("output={:?}", PositiveNonzeroInteger::new(x)?); - Ok(()) -} - -// Don't change anything below this line. - -#[derive(PartialEq, Debug)] -struct PositiveNonzeroInteger(u64); #[derive(PartialEq, Debug)] enum CreationError { @@ -40,17 +20,7 @@ enum CreationError { Zero, } -impl PositiveNonzeroInteger { - fn new(value: i64) -> Result { - match value { - x if x < 0 => Err(CreationError::Negative), - x if x == 0 => Err(CreationError::Zero), - x => Ok(PositiveNonzeroInteger(x as u64)), - } - } -} - -// This is required so that `CreationError` can implement `error::Error`. +// This is required so that `CreationError` can implement `Error`. impl fmt::Display for CreationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let description = match *self { @@ -61,4 +31,26 @@ impl fmt::Display for CreationError { } } -impl error::Error for CreationError {} +impl Error for CreationError {} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + 0 => Err(CreationError::Zero), + x if x < 0 => Err(CreationError::Negative), + x => Ok(PositiveNonzeroInteger(x as u64)), + } + } +} + +// TODO: Add the correct return type `Result<(), Box>`. What can we +// use to describe both errors? Is there a trait which both errors implement? +fn main() { + let pretend_user_input = "42"; + let x: i64 = pretend_user_input.parse()?; + println!("output={:?}", PositiveNonzeroInteger::new(x)?); + Ok(()) +} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index d39044c0..700c1792 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -692,24 +692,23 @@ name = "errors5" dir = "13_error_handling" test = false hint = """ -There are two different possible `Result` types produced within `main()`, which -are propagated using `?` operators. How do we declare a return type from -`main()` that allows both? +There are two different possible `Result` types produced within the `main` +function, which are propagated using the `?` operators. How do we declare a +return type for the `main` function that allows both? Under the hood, the `?` operator calls `From::from` on the error value to -convert it to a boxed trait object, a `Box`. This boxed trait -object is polymorphic, and since all errors implement the `error::Error` trait, -we can capture lots of different errors in one "Box" object. +convert it to a boxed trait object, a `Box`. This boxed trait object +is polymorphic, and since all errors implement the `Error` trait, we can capture +lots of different errors in one `Box` object. -Check out this section of the book: +Check out this section of The Book: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator Read more about boxing errors: https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/boxing_errors.html Read more about using the `?` operator with boxed errors: -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html -""" +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] name = "errors6" diff --git a/solutions/13_error_handling/errors5.rs b/solutions/13_error_handling/errors5.rs index 4e181989..c1424eee 100644 --- a/solutions/13_error_handling/errors5.rs +++ b/solutions/13_error_handling/errors5.rs @@ -1 +1,54 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This exercise is an altered version of the `errors4` exercise. It uses some +// concepts that we won't get to until later in the course, like `Box` and the +// `From` trait. It's not important to understand them in detail right now, but +// you can read ahead if you like. For now, think of the `Box` type as +// an "I want anything that does ???" type. +// +// In short, this particular use case for boxes is for when you want to own a +// value and you care only that it is a type which implements a particular +// trait. To do so, The `Box` is declared as of type `Box` where +// `Trait` is the trait the compiler looks for on any value used in that +// context. For this exercise, that context is the potential errors which +// can be returned in a `Result`. + +use std::error::Error; +use std::fmt; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +// This is required so that `CreationError` can implement `Error`. +impl fmt::Display for CreationError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = match *self { + CreationError::Negative => "number is negative", + CreationError::Zero => "number is zero", + }; + f.write_str(description) + } +} + +impl Error for CreationError {} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + x if x < 0 => Err(CreationError::Negative), + 0 => Err(CreationError::Zero), + x => Ok(PositiveNonzeroInteger(x as u64)), + } + } +} + +fn main() -> Result<(), Box> { + let pretend_user_input = "42"; + let x: i64 = pretend_user_input.parse()?; + println!("output={:?}", PositiveNonzeroInteger::new(x)?); + Ok(()) +} From b1daea1fe8536d7b7b4463cb8fc36d69848ef77a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 01:12:50 +0200 Subject: [PATCH 379/433] errors6 solution --- exercises/13_error_handling/errors6.rs | 87 ++++++++++++------------ rustlings-macros/info.toml | 10 +-- solutions/13_error_handling/errors6.rs | 93 +++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 52 deletions(-) diff --git a/exercises/13_error_handling/errors6.rs b/exercises/13_error_handling/errors6.rs index 8b08e086..0652abda 100644 --- a/exercises/13_error_handling/errors6.rs +++ b/exercises/13_error_handling/errors6.rs @@ -1,52 +1,51 @@ -// Using catch-all error types like `Box` isn't recommended -// for library code, where callers might want to make decisions based on the -// error content, instead of printing it out or propagating it further. Here, we -// define a custom error type to make it possible for callers to decide what to -// do next when our function returns an error. +// Using catch-all error types like `Box` isn't recommended for +// library code where callers might want to make decisions based on the error +// content instead of printing it out or propagating it further. Here, we define +// a custom error type to make it possible for callers to decide what to do next +// when our function returns an error. use std::num::ParseIntError; -// This is a custom error type that we will be using in `parse_pos_nonzero()`. -#[derive(PartialEq, Debug)] -enum ParsePosNonzeroError { - Creation(CreationError), - ParseInt(ParseIntError), -} - -impl ParsePosNonzeroError { - fn from_creation(err: CreationError) -> ParsePosNonzeroError { - ParsePosNonzeroError::Creation(err) - } - // TODO: add another error conversion function here. - // fn from_parseint... -} - -fn parse_pos_nonzero(s: &str) -> Result { - // TODO: change this to return an appropriate error instead of panicking - // when `parse()` returns an error. - let x: i64 = s.parse().unwrap(); - PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation) -} - -// Don't change anything below this line. - -#[derive(PartialEq, Debug)] -struct PositiveNonzeroInteger(u64); - #[derive(PartialEq, Debug)] enum CreationError { Negative, Zero, } +// A custom error type that we will be using in `PositiveNonzeroInteger::parse`. +#[derive(PartialEq, Debug)] +enum ParsePosNonzeroError { + Creation(CreationError), + ParseInt(ParseIntError), +} + +impl ParsePosNonzeroError { + fn from_creation(err: CreationError) -> Self { + Self::Creation(err) + } + + // TODO: Add another error conversion function here. + // fn from_parseint(???) -> Self { ??? } +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + impl PositiveNonzeroInteger { - fn new(value: i64) -> Result { + fn new(value: i64) -> Result { match value { x if x < 0 => Err(CreationError::Negative), x if x == 0 => Err(CreationError::Zero), - x => Ok(PositiveNonzeroInteger(x as u64)), + x => Ok(Self(x as u64)), } } + + fn parse(s: &str) -> Result { + // TODO: change this to return an appropriate error instead of panicking + // when `parse()` returns an error. + let x: i64 = s.parse().unwrap(); + Self::new(x).map_err(ParsePosNonzeroError::from_creation) + } } fn main() { @@ -56,36 +55,36 @@ fn main() { #[cfg(test)] mod test { use super::*; + use std::num::IntErrorKind; #[test] fn test_parse_error() { - // We can't construct a ParseIntError, so we have to pattern match. assert!(matches!( - parse_pos_nonzero("not a number"), - Err(ParsePosNonzeroError::ParseInt(_)) + PositiveNonzeroInteger::parse("not a number"), + Err(ParsePosNonzeroError::ParseInt(_)), )); } #[test] fn test_negative() { assert_eq!( - parse_pos_nonzero("-555"), - Err(ParsePosNonzeroError::Creation(CreationError::Negative)) + PositiveNonzeroInteger::parse("-555"), + Err(ParsePosNonzeroError::Creation(CreationError::Negative)), ); } #[test] fn test_zero() { assert_eq!( - parse_pos_nonzero("0"), - Err(ParsePosNonzeroError::Creation(CreationError::Zero)) + PositiveNonzeroInteger::parse("0"), + Err(ParsePosNonzeroError::Creation(CreationError::Zero)), ); } #[test] fn test_positive() { - let x = PositiveNonzeroInteger::new(42); - assert!(x.is_ok()); - assert_eq!(parse_pos_nonzero("42"), Ok(x.unwrap())); + let x = PositiveNonzeroInteger::new(42).unwrap(); + assert_eq!(x.0, 42); + assert_eq!(PositiveNonzeroInteger::parse("42"), Ok(x)); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 700c1792..dc288c0c 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -714,17 +714,13 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen name = "errors6" dir = "13_error_handling" hint = """ -This exercise uses a completed version of `PositiveNonzeroInteger` from -errors4. +This exercise uses a completed version of `PositiveNonzeroInteger` from the +previous exercises. Below the line that `TODO` asks you to change, there is an example of using the `map_err()` method on a `Result` to transform one type of error into another. Try using something similar on the `Result` from `parse()`. You -might use the `?` operator to return early from the function, or you might -use a `match` expression, or maybe there's another way! - -You can create another function inside `impl ParsePosNonzeroError` to use -with `map_err()`. +can then use the `?` operator to return early. Read more about `map_err()` in the `std::result` documentation: https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err""" diff --git a/solutions/13_error_handling/errors6.rs b/solutions/13_error_handling/errors6.rs index 4e181989..70680cf0 100644 --- a/solutions/13_error_handling/errors6.rs +++ b/solutions/13_error_handling/errors6.rs @@ -1 +1,92 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Using catch-all error types like `Box` isn't recommended for +// library code where callers might want to make decisions based on the error +// content instead of printing it out or propagating it further. Here, we define +// a custom error type to make it possible for callers to decide what to do next +// when our function returns an error. + +use std::num::ParseIntError; + +#[derive(PartialEq, Debug)] +enum CreationError { + Negative, + Zero, +} + +// A custom error type that we will be using in `PositiveNonzeroInteger::parse`. +#[derive(PartialEq, Debug)] +enum ParsePosNonzeroError { + Creation(CreationError), + ParseInt(ParseIntError), +} + +impl ParsePosNonzeroError { + fn from_creation(err: CreationError) -> Self { + Self::Creation(err) + } + + fn from_parseint(err: ParseIntError) -> Self { + Self::ParseInt(err) + } +} + +#[derive(PartialEq, Debug)] +struct PositiveNonzeroInteger(u64); + +impl PositiveNonzeroInteger { + fn new(value: i64) -> Result { + match value { + x if x < 0 => Err(CreationError::Negative), + x if x == 0 => Err(CreationError::Zero), + x => Ok(Self(x as u64)), + } + } + + fn parse(s: &str) -> Result { + // Return an appropriate error instead of panicking when `parse()` + // returns an error. + let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Self::new(x).map_err(ParsePosNonzeroError::from_creation) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod test { + use super::*; + use std::num::IntErrorKind; + + #[test] + fn test_parse_error() { + assert!(matches!( + PositiveNonzeroInteger::parse("not a number"), + Err(ParsePosNonzeroError::ParseInt(_)), + )); + } + + #[test] + fn test_negative() { + assert_eq!( + PositiveNonzeroInteger::parse("-555"), + Err(ParsePosNonzeroError::Creation(CreationError::Negative)), + ); + } + + #[test] + fn test_zero() { + assert_eq!( + PositiveNonzeroInteger::parse("0"), + Err(ParsePosNonzeroError::Creation(CreationError::Zero)), + ); + } + + #[test] + fn test_positive() { + let x = PositiveNonzeroInteger::new(42).unwrap(); + assert_eq!(x.0, 42); + assert_eq!(PositiveNonzeroInteger::parse("42"), Ok(x)); + } +} From 46121b71cf2f4da296e80fad025eaee03c67dcd5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 02:00:08 +0200 Subject: [PATCH 380/433] generics1 rewrite and solution --- exercises/14_generics/generics1.rs | 19 +++++++++++++++---- rustlings-macros/info.toml | 7 ++++++- solutions/14_generics/generics1.rs | 18 +++++++++++++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/exercises/14_generics/generics1.rs b/exercises/14_generics/generics1.rs index c023e644..87ed990b 100644 --- a/exercises/14_generics/generics1.rs +++ b/exercises/14_generics/generics1.rs @@ -1,7 +1,18 @@ -// This shopping list program isn't compiling! Use your knowledge of generics to -// fix it. +// `Vec` is generic over the type `T`. In most cases, the compiler is able to +// infer `T`, for example after pushing a value with a concrete type to the vector. +// But in this exercise, the compiler needs some help through a type annotation. fn main() { - let mut shopping_list: Vec = Vec::new(); - shopping_list.push("milk"); + // TODO: Fix the compiler error by annotating the type of the vector + // `Vec`. Choose `T` as some integer type that can be created from + // `u8` and `i8`. + let mut numbers = Vec::new(); + + // Don't change the lines below. + let n1: u8 = 42; + numbers.push(n1.into()); + let n2: i8 = -1; + numbers.push(n2.into()); + + println!("{numbers:?}"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index dc288c0c..23eb3048 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -734,8 +734,13 @@ test = false hint = """ Vectors in Rust make use of generics to create dynamically sized arrays of any type. +If the vector `numbers` has the type `Vec`, then we can only push values of +type `T` to it. By using `into()` before pushing, we ask the compiler to convert +`n1` and `n2` to `T`. But the compiler doesn't know what `T` is yet and needs a +type annotation. -You need to tell the compiler what type we are pushing onto this vector.""" +`u8` and `i8` can both be converted to `i16`, `i32` and `i64`. Choose one for +the generic of the vector.""" [[exercises]] name = "generics2" diff --git a/solutions/14_generics/generics1.rs b/solutions/14_generics/generics1.rs index 4e181989..e2195fd3 100644 --- a/solutions/14_generics/generics1.rs +++ b/solutions/14_generics/generics1.rs @@ -1 +1,17 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// `Vec` is generic over the type `T`. In most cases, the compiler is able to +// infer `T`, for example after pushing a value with a concrete type to the vector. +// But in this exercise, the compiler needs some help through a type annotation. + +fn main() { + // `u8` and `i8` can both be converted to `i16`. + let mut numbers: Vec = Vec::new(); + // ^^^^^^^^^^ added + + // Don't change the lines below. + let n1: u8 = 42; + numbers.push(n1.into()); + let n2: i8 = -1; + numbers.push(n2.into()); + + println!("{numbers:?}"); +} From de3f846a53055bbca5ec9dd6d536a31c82d39648 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 02:25:11 +0200 Subject: [PATCH 381/433] generics2 solution --- exercises/14_generics/generics2.rs | 4 ++-- rustlings-macros/info.toml | 8 ++------ solutions/14_generics/generics2.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/exercises/14_generics/generics2.rs b/exercises/14_generics/generics2.rs index 6cdcdaf5..8908725b 100644 --- a/exercises/14_generics/generics2.rs +++ b/exercises/14_generics/generics2.rs @@ -1,10 +1,10 @@ // This powerful wrapper provides the ability to store a positive integer value. -// Rewrite it using generics so that it supports wrapping ANY type. - +// TODO: Rewrite it using a generic so that it supports wrapping ANY type. struct Wrapper { value: u32, } +// TODO: Adapt the struct's implementation to be generic over the wrapped value. impl Wrapper { fn new(value: u32) -> Self { Wrapper { value } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 23eb3048..11d6d59f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -746,12 +746,8 @@ the generic of the vector.""" name = "generics2" dir = "14_generics" hint = """ -Currently we are wrapping only values of type `u32`. - -Maybe we could update the explicit references to this data type somehow? - -If you are still stuck https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions -""" +Related section in The Book: +https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions""" # TRAITS diff --git a/solutions/14_generics/generics2.rs b/solutions/14_generics/generics2.rs index 4e181989..14f3f7af 100644 --- a/solutions/14_generics/generics2.rs +++ b/solutions/14_generics/generics2.rs @@ -1 +1,28 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +struct Wrapper { + value: T, +} + +impl Wrapper { + fn new(value: T) -> Self { + Wrapper { value } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn store_u32_in_wrapper() { + assert_eq!(Wrapper::new(42).value, 42); + } + + #[test] + fn store_str_in_wrapper() { + assert_eq!(Wrapper::new("Foo").value, "Foo"); + } +} From 789223cc9e247eb9da90698b1c3011c26cdc863c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 03:04:57 +0200 Subject: [PATCH 382/433] traits1 solution --- exercises/15_traits/traits1.rs | 17 ++++++----------- solutions/15_traits/traits1.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/exercises/15_traits/traits1.rs b/exercises/15_traits/traits1.rs index b17c9c6e..85be17ea 100644 --- a/exercises/15_traits/traits1.rs +++ b/exercises/15_traits/traits1.rs @@ -1,19 +1,17 @@ -// Time to implement some traits! Your task is to implement the trait -// `AppendBar` for the type `String`. The trait AppendBar has only one function, -// which appends "Bar" to any object implementing this trait. - +// The trait `AppendBar` has only one function which appends "Bar" to any object +// implementing this trait. trait AppendBar { fn append_bar(self) -> Self; } impl AppendBar for String { - // TODO: Implement `AppendBar` for type `String`. + // TODO: Implement `AppendBar` for the type `String`. } fn main() { let s = String::from("Foo"); let s = s.append_bar(); - println!("s: {}", s); + println!("s: {s}"); } #[cfg(test)] @@ -22,14 +20,11 @@ mod tests { #[test] fn is_foo_bar() { - assert_eq!(String::from("Foo").append_bar(), String::from("FooBar")); + assert_eq!(String::from("Foo").append_bar(), "FooBar"); } #[test] fn is_bar_bar() { - assert_eq!( - String::from("").append_bar().append_bar(), - String::from("BarBar") - ); + assert_eq!(String::from("").append_bar().append_bar(), "BarBar"); } } diff --git a/solutions/15_traits/traits1.rs b/solutions/15_traits/traits1.rs index 4e181989..790873f4 100644 --- a/solutions/15_traits/traits1.rs +++ b/solutions/15_traits/traits1.rs @@ -1 +1,32 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The trait `AppendBar` has only one function which appends "Bar" to any object +// implementing this trait. +trait AppendBar { + fn append_bar(self) -> Self; +} + +impl AppendBar for String { + fn append_bar(self) -> Self { + self + "Bar" + } +} + +fn main() { + let s = String::from("Foo"); + let s = s.append_bar(); + println!("s: {s}"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_foo_bar() { + assert_eq!(String::from("Foo").append_bar(), "FooBar"); + } + + #[test] + fn is_bar_bar() { + assert_eq!(String::from("").append_bar().append_bar(), "BarBar"); + } +} From 091e1e7f7a1afad539479674e06cae7c8d8dab7f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 11:58:44 +0200 Subject: [PATCH 383/433] traits2 solution --- exercises/15_traits/traits2.rs | 13 ++++--------- rustlings-macros/info.toml | 15 ++++++--------- solutions/15_traits/traits2.rs | 28 +++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index 170779b2..e9040162 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -1,14 +1,9 @@ -// Your task is to implement the trait `AppendBar` for a vector of strings. To -// implement this trait, consider for a moment what it means to 'append "Bar"' -// to a vector of strings. -// -// No boiler plate code this time, you can do this! - trait AppendBar { fn append_bar(self) -> Self; } -// TODO: Implement trait `AppendBar` for a vector of strings. +// TODO: Implement the trait `AppendBar` for a vector of strings. +// `appned_bar` should push the string "Bar" into the vector. fn main() { // You can optionally experiment here. @@ -21,7 +16,7 @@ mod tests { #[test] fn is_vec_pop_eq_bar() { let mut foo = vec![String::from("Foo")].append_bar(); - assert_eq!(foo.pop().unwrap(), String::from("Bar")); - assert_eq!(foo.pop().unwrap(), String::from("Foo")); + assert_eq!(foo.pop().unwrap(), "Bar"); + assert_eq!(foo.pop().unwrap(), "Foo"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 2cc1db65..604f6741 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -755,21 +755,18 @@ https://doc.rust-lang.org/stable/book/ch10-01-syntax.html#in-method-definitions" name = "traits1" dir = "15_traits" hint = """ -A discussion about Traits in Rust can be found at: -https://doc.rust-lang.org/book/ch10-02-traits.html -""" +More about traits in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html""" [[exercises]] name = "traits2" dir = "15_traits" hint = """ -Notice how the trait takes ownership of `self`, and returns `Self`. +Notice how the trait takes ownership of `self` and returns `Self`. -Try mutating the incoming string vector. Have a look at the tests to see -what the result should look like! - -Vectors provide suitable methods for adding an element at the end. See -the documentation at: https://doc.rust-lang.org/std/vec/struct.Vec.html""" +Although the signature of `append_bar` in the trait takes `self` as argument, +the implementation can take `mut self` instead. This is possible because the +the value is owned anyway.""" [[exercises]] name = "traits3" diff --git a/solutions/15_traits/traits2.rs b/solutions/15_traits/traits2.rs index 4e181989..0db93e0f 100644 --- a/solutions/15_traits/traits2.rs +++ b/solutions/15_traits/traits2.rs @@ -1 +1,27 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +trait AppendBar { + fn append_bar(self) -> Self; +} + +impl AppendBar for Vec { + fn append_bar(mut self) -> Self { + // ^^^ this is important + self.push(String::from("Bar")); + self + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_vec_pop_eq_bar() { + let mut foo = vec![String::from("Foo")].append_bar(); + assert_eq!(foo.pop().unwrap(), "Bar"); + assert_eq!(foo.pop().unwrap(), "Foo"); + } +} From c07209b635d6adf6fcadcbcca14942bf76a0a978 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 12:00:28 +0200 Subject: [PATCH 384/433] Unify info.toml --- rustlings-macros/info.toml | 40 +++++++++++++------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 604f6741..f5dd82ad 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -776,8 +776,7 @@ Traits can have a default implementation for functions. Structs that implement the trait can then use the default version of these functions if they choose not to implement the function themselves. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations -""" +See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations""" [[exercises]] name = "traits4" @@ -786,8 +785,7 @@ hint = """ Instead of using concrete types as parameters you can use traits. Try replacing the '??' with 'impl [what goes here?]' -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters -""" +See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters""" [[exercises]] name = "traits5" @@ -797,8 +795,7 @@ hint = """ To ensure a parameter implements multiple traits use the '+ syntax'. Try replacing the '??' with 'impl [what goes here?] + [what goes here?]'. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax -""" +See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax""" # QUIZ 3 @@ -913,8 +910,7 @@ Step 4: An iterator goes through all elements in a collection, but what if we've run out of elements? What should we expect here? If you're stuck, take a look at -https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas. -""" +https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas.""" [[exercises]] name = "iterators2" @@ -1008,8 +1004,7 @@ assertions). For a non-empty list keep in mind that we want to use our `Cons` "list builder". Although the current list is one of integers (`i32`), feel free to change the -definition and try other types! -""" +definition and try other types!""" [[exercises]] name = "rc1" @@ -1026,8 +1021,7 @@ In the end the `Sun` only has one reference again, to itself. See more at: https://doc.rust-lang.org/book/ch15-04-rc.html -* Unfortunately Pluto is no longer considered a planet :( -""" +* Unfortunately Pluto is no longer considered a planet :(""" [[exercises]] name = "arc1" @@ -1042,10 +1036,9 @@ inside the loop but still in the main thread. thread-local copy of the numbers. This is a simple exercise if you understand the underlying concepts, but if this -is too much of a struggle, consider reading through all of Chapter 16 in the -book: -https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html -""" +is too much of a struggle, consider reading through all of Chapter 16 in The +Book: +https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html""" [[exercises]] name = "cow1" @@ -1055,8 +1048,7 @@ If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is called. Check out https://doc.rust-lang.org/std/borrow/enum.Cow.html for documentation -on the `Cow` type. -""" +on the `Cow` type.""" # THREADS @@ -1075,8 +1067,7 @@ https://doc.rust-lang.org/book/ch16-01-threads.html#waiting-for-all-threads-to-f Use the `JoinHandle`s to wait for each thread to finish and collect their results. -https://doc.rust-lang.org/std/thread/struct.JoinHandle.html -""" +https://doc.rust-lang.org/std/thread/struct.JoinHandle.html""" [[exercises]] name = "threads2" @@ -1097,8 +1088,7 @@ let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 })); ``` Similar to the code in the following example in the book: -https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads -""" +https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads""" [[exercises]] name = "threads3" @@ -1113,8 +1103,7 @@ one thread and receive them in another. Multiple producers are possible by using clone() to create a duplicate of the original sending end. -See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info. -""" +See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info.""" # MACROS @@ -1230,8 +1219,7 @@ or a closure to wrap the error from `parse::`. Yet another hint: If you would like to propagate errors by using the `?` operator in your solution, you might want to look at -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html -""" +https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html""" [[exercises]] name = "try_from_into" From b4b7ae63ada9128d4798d301cfc757a60904c6b8 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 12:11:57 +0200 Subject: [PATCH 385/433] traits3 solution --- exercises/15_traits/traits3.rs | 15 +++++++------- rustlings-macros/info.toml | 9 +++++---- solutions/15_traits/traits3.rs | 37 +++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/exercises/15_traits/traits3.rs b/exercises/15_traits/traits3.rs index 66da235f..c244650d 100644 --- a/exercises/15_traits/traits3.rs +++ b/exercises/15_traits/traits3.rs @@ -1,9 +1,8 @@ -// Your task is to implement the Licensed trait for both structures and have -// them return the same information without writing the same function twice. -// -// Consider what you can add to the Licensed trait. - trait Licensed { + // TODO: Add a default implementation for `licensing_info` so that + // implementors like the two structs below can share that default behavior + // without repeating the function. + // The default license information should be the string "Default license". fn licensing_info(&self) -> String; } @@ -15,8 +14,8 @@ struct OtherSoftware { version_number: String, } -impl Licensed for SomeSoftware {} // Don't edit this line -impl Licensed for OtherSoftware {} // Don't edit this line +impl Licensed for SomeSoftware {} // Don't edit this line. +impl Licensed for OtherSoftware {} // Don't edit this line. fn main() { // You can optionally experiment here. @@ -28,7 +27,7 @@ mod tests { #[test] fn is_licensing_info_the_same() { - let licensing_info = String::from("Some information"); + let licensing_info = "Default license"; let some_software = SomeSoftware { version_number: 1 }; let other_software = OtherSoftware { version_number: "v2.0.0".to_string(), diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index f5dd82ad..92e440ae 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -772,11 +772,12 @@ the value is owned anyway.""" name = "traits3" dir = "15_traits" hint = """ -Traits can have a default implementation for functions. Structs that implement -the trait can then use the default version of these functions if they choose not -to implement the function themselves. +Traits can have a default implementation for functions. Data types that +implement the trait can then use the default version of these functions +if they choose not to implement the function themselves. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations""" +Related section in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html#default-implementations""" [[exercises]] name = "traits4" diff --git a/solutions/15_traits/traits3.rs b/solutions/15_traits/traits3.rs index 4e181989..3d8ec85e 100644 --- a/solutions/15_traits/traits3.rs +++ b/solutions/15_traits/traits3.rs @@ -1 +1,36 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +trait Licensed { + fn licensing_info(&self) -> String { + "Default license".to_string() + } +} + +struct SomeSoftware { + version_number: i32, +} + +struct OtherSoftware { + version_number: String, +} + +impl Licensed for SomeSoftware {} +impl Licensed for OtherSoftware {} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_licensing_info_the_same() { + let licensing_info = "Default license"; + let some_software = SomeSoftware { version_number: 1 }; + let other_software = OtherSoftware { + version_number: "v2.0.0".to_string(), + }; + assert_eq!(some_software.licensing_info(), licensing_info); + assert_eq!(other_software.licensing_info(), licensing_info); + } +} From c0452d160b889b3686409820192797d9e9f9cad7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 12:23:33 +0200 Subject: [PATCH 386/433] traits4 solution --- exercises/15_traits/traits4.rs | 27 ++++++++------------------ rustlings-macros/info.toml | 5 +++-- solutions/15_traits/traits4.rs | 35 +++++++++++++++++++++++++++++++++- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/exercises/15_traits/traits4.rs b/exercises/15_traits/traits4.rs index ed63f6e1..80092a64 100644 --- a/exercises/15_traits/traits4.rs +++ b/exercises/15_traits/traits4.rs @@ -1,23 +1,18 @@ -// Your task is to replace the '??' sections so the code compiles. -// -// Don't change any line other than the marked one. - trait Licensed { fn licensing_info(&self) -> String { - "some information".to_string() + "Default license".to_string() } } -struct SomeSoftware {} - -struct OtherSoftware {} +struct SomeSoftware; +struct OtherSoftware; impl Licensed for SomeSoftware {} impl Licensed for OtherSoftware {} -// YOU MAY ONLY CHANGE THE NEXT LINE -fn compare_license_types(software: ??, software_two: ??) -> bool { - software.licensing_info() == software_two.licensing_info() +// TODO: Fix the compiler error by only changing the signature of this function. +fn compare_license_types(software1: ???, software2: ???) -> bool { + software1.licensing_info() == software2.licensing_info() } fn main() { @@ -30,17 +25,11 @@ mod tests { #[test] fn compare_license_information() { - let some_software = SomeSoftware {}; - let other_software = OtherSoftware {}; - - assert!(compare_license_types(some_software, other_software)); + assert!(compare_license_types(SomeSoftware, OtherSoftware)); } #[test] fn compare_license_information_backwards() { - let some_software = SomeSoftware {}; - let other_software = OtherSoftware {}; - - assert!(compare_license_types(other_software, some_software)); + assert!(compare_license_types(OtherSoftware, SomeSoftware)); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 92e440ae..43ccdf80 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -784,9 +784,10 @@ name = "traits4" dir = "15_traits" hint = """ Instead of using concrete types as parameters you can use traits. Try replacing -the '??' with 'impl [what goes here?]' +`???` with `impl [what goes here?]`. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters""" +Related section in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters""" [[exercises]] name = "traits5" diff --git a/solutions/15_traits/traits4.rs b/solutions/15_traits/traits4.rs index 4e181989..78b5a110 100644 --- a/solutions/15_traits/traits4.rs +++ b/solutions/15_traits/traits4.rs @@ -1 +1,34 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +trait Licensed { + fn licensing_info(&self) -> String { + "Default license".to_string() + } +} + +struct SomeSoftware; +struct OtherSoftware; + +impl Licensed for SomeSoftware {} +impl Licensed for OtherSoftware {} + +fn compare_license_types(software1: impl Licensed, software2: impl Licensed) -> bool { + software1.licensing_info() == software2.licensing_info() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compare_license_information() { + assert!(compare_license_types(SomeSoftware, OtherSoftware)); + } + + #[test] + fn compare_license_information_backwards() { + assert!(compare_license_types(OtherSoftware, SomeSoftware)); + } +} From db4d649e557f34641f2c7cc197dff2fb29637a7f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 12:27:53 +0200 Subject: [PATCH 387/433] Remove move_semantics6 --- dev/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 3283871a..2c5eaf09 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -60,8 +60,6 @@ bin = [ { name = "move_semantics4_sol", path = "../solutions/06_move_semantics/move_semantics4.rs" }, { name = "move_semantics5", path = "../exercises/06_move_semantics/move_semantics5.rs" }, { name = "move_semantics5_sol", path = "../solutions/06_move_semantics/move_semantics5.rs" }, - { name = "move_semantics6", path = "../exercises/06_move_semantics/move_semantics6.rs" }, - { name = "move_semantics6_sol", path = "../solutions/06_move_semantics/move_semantics6.rs" }, { name = "structs1", path = "../exercises/07_structs/structs1.rs" }, { name = "structs1_sol", path = "../solutions/07_structs/structs1.rs" }, { name = "structs2", path = "../exercises/07_structs/structs2.rs" }, From 45cfe86fb05a21dd52d9d72d07e881037803395d Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 12:29:25 +0200 Subject: [PATCH 388/433] traits5 solution --- exercises/15_traits/traits5.rs | 28 ++++++++++++++---------- rustlings-macros/info.toml | 6 ++--- solutions/15_traits/traits5.rs | 40 +++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/exercises/15_traits/traits5.rs b/exercises/15_traits/traits5.rs index 3e62283f..5b356ac7 100644 --- a/exercises/15_traits/traits5.rs +++ b/exercises/15_traits/traits5.rs @@ -1,7 +1,3 @@ -// Your task is to replace the '??' sections so the code compiles. -// -// Don't change any line other than the marked one. - trait SomeTrait { fn some_function(&self) -> bool { true @@ -14,20 +10,30 @@ trait OtherTrait { } } -struct SomeStruct {} -struct OtherStruct {} - +struct SomeStruct; impl SomeTrait for SomeStruct {} impl OtherTrait for SomeStruct {} + +struct OtherStruct; impl SomeTrait for OtherStruct {} impl OtherTrait for OtherStruct {} -// YOU MAY ONLY CHANGE THE NEXT LINE -fn some_func(item: ??) -> bool { +// TODO: Fix the compiler error by only changing the signature of this function. +fn some_func(item: ???) -> bool { item.some_function() && item.other_function() } fn main() { - some_func(SomeStruct {}); - some_func(OtherStruct {}); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_some_func() { + assert!(some_func(SomeStruct)); + assert!(some_func(OtherStruct)); + } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 43ccdf80..92c878fd 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -792,12 +792,12 @@ https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters""" [[exercises]] name = "traits5" dir = "15_traits" -test = false hint = """ To ensure a parameter implements multiple traits use the '+ syntax'. Try -replacing the '??' with 'impl [what goes here?] + [what goes here?]'. +replacing `???` with 'impl [what goes here?] + [what goes here?]'. -See the documentation at: https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax""" +Related section in The Book: +https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bounds-with-the--syntax""" # QUIZ 3 diff --git a/solutions/15_traits/traits5.rs b/solutions/15_traits/traits5.rs index 4e181989..1fb426a4 100644 --- a/solutions/15_traits/traits5.rs +++ b/solutions/15_traits/traits5.rs @@ -1 +1,39 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +trait SomeTrait { + fn some_function(&self) -> bool { + true + } +} + +trait OtherTrait { + fn other_function(&self) -> bool { + true + } +} + +struct SomeStruct; +impl SomeTrait for SomeStruct {} +impl OtherTrait for SomeStruct {} + +struct OtherStruct; +impl SomeTrait for OtherStruct {} +impl OtherTrait for OtherStruct {} + +fn some_func(item: impl SomeTrait + OtherTrait) -> bool { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + item.some_function() && item.other_function() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_some_func() { + assert!(some_func(SomeStruct)); + assert!(some_func(OtherStruct)); + } +} From c1707404231e5a1d7bc837e21faf34fdc51db0bf Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 12:29:35 +0200 Subject: [PATCH 389/433] Highlight change in traits4 solution --- solutions/15_traits/traits4.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/solutions/15_traits/traits4.rs b/solutions/15_traits/traits4.rs index 78b5a110..3675b8df 100644 --- a/solutions/15_traits/traits4.rs +++ b/solutions/15_traits/traits4.rs @@ -11,6 +11,7 @@ impl Licensed for SomeSoftware {} impl Licensed for OtherSoftware {} fn compare_license_types(software1: impl Licensed, software2: impl Licensed) -> bool { + // ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ software1.licensing_info() == software2.licensing_info() } From 64c2de95ca95c1d23dcb416723b33ccdfca9c956 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 13:01:52 +0200 Subject: [PATCH 390/433] quiz3 solution --- exercises/quizzes/quiz3.rs | 18 +++++----- rustlings-macros/info.toml | 12 +++++-- solutions/quizzes/quiz3.rs | 70 +++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/exercises/quizzes/quiz3.rs b/exercises/quizzes/quiz3.rs index f3cb1bcf..c877c5f8 100644 --- a/exercises/quizzes/quiz3.rs +++ b/exercises/quizzes/quiz3.rs @@ -3,26 +3,27 @@ // - Traits // // An imaginary magical school has a new report card generation system written -// in Rust! Currently the system only supports creating report cards where the +// in Rust! Currently, the system only supports creating report cards where the // student's grade is represented numerically (e.g. 1.0 -> 5.5). However, the // school also issues alphabetical grades (A+ -> F-) and needs to be able to // print both types of report card! // -// Make the necessary code changes in the struct ReportCard and the impl block -// to support alphabetical report cards. Change the Grade in the second test to -// "A+" to show that your changes allow alphabetical grades. +// Make the necessary code changes in the struct `ReportCard` and the impl +// block to support alphabetical report cards in addition to numerical ones. +// TODO: Adjust the struct as described above. struct ReportCard { grade: f32, student_name: String, student_age: u8, } +// TODO: Adjust the impl block as described above. impl ReportCard { fn print(&self) -> String { format!( "{} ({}) - achieved a grade of {}", - &self.student_name, &self.student_age, &self.grade + &self.student_name, &self.student_age, &self.grade, ) } } @@ -44,21 +45,20 @@ mod tests { }; assert_eq!( report_card.print(), - "Tom Wriggle (12) - achieved a grade of 2.1" + "Tom Wriggle (12) - achieved a grade of 2.1", ); } #[test] fn generate_alphabetic_report_card() { - // TODO: Make sure to change the grade here after you finish the exercise. let report_card = ReportCard { - grade: 2.1, + grade: "A+", student_name: "Gary Plotter".to_string(), student_age: 11, }; assert_eq!( report_card.print(), - "Gary Plotter (11) - achieved a grade of A+" + "Gary Plotter (11) - achieved a grade of A+", ); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 92c878fd..bed2eafa 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -805,10 +805,16 @@ https://doc.rust-lang.org/book/ch10-02-traits.html#specifying-multiple-trait-bou name = "quiz3" dir = "quizzes" hint = """ -To find the best solution to this challenge you're going to need to think back -to your knowledge of traits, specifically 'Trait Bound Syntax' +To find the best solution to this challenge, you need to recall your knowledge +of traits, specifically "Trait Bound Syntax": +https://doc.rust-lang.org/book/ch10-02-traits.html#trait-bound-syntax -You may also need this: `use std::fmt::Display;`.""" +Here is how to specify a trait bound for an implementation block: +`impl for Foo { … }` + +You may need this: +`use std::fmt::Display;` +""" # LIFETIMES diff --git a/solutions/quizzes/quiz3.rs b/solutions/quizzes/quiz3.rs index 4e181989..e3413fd0 100644 --- a/solutions/quizzes/quiz3.rs +++ b/solutions/quizzes/quiz3.rs @@ -1 +1,69 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This quiz tests: +// - Generics +// - Traits +// +// An imaginary magical school has a new report card generation system written +// in Rust! Currently, the system only supports creating report cards where the +// student's grade is represented numerically (e.g. 1.0 -> 5.5). However, the +// school also issues alphabetical grades (A+ -> F-) and needs to be able to +// print both types of report card! +// +// Make the necessary code changes in the struct `ReportCard` and the impl +// block to support alphabetical report cards in addition to numerical ones. + +use std::fmt::Display; + +// Make the struct generic over `T`. +struct ReportCard { + // ^^^ + grade: T, + // ^ + student_name: String, + student_age: u8, +} + +// To be able to print the grade, it has to implement the `Display` trait. +impl ReportCard { + // ^^^^^^^ require that `T` implements `Display`. + fn print(&self) -> String { + format!( + "{} ({}) - achieved a grade of {}", + &self.student_name, &self.student_age, &self.grade, + ) + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_numeric_report_card() { + let report_card = ReportCard { + grade: 2.1, + student_name: "Tom Wriggle".to_string(), + student_age: 12, + }; + assert_eq!( + report_card.print(), + "Tom Wriggle (12) - achieved a grade of 2.1", + ); + } + + #[test] + fn generate_alphabetic_report_card() { + let report_card = ReportCard { + grade: "A+", + student_name: "Gary Plotter".to_string(), + student_age: 11, + }; + assert_eq!( + report_card.print(), + "Gary Plotter (11) - achieved a grade of A+", + ); + } +} From 7efccc36b4c26c444eab2531b6139190af569d6f Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 13:24:21 +0200 Subject: [PATCH 391/433] lifetimes1 solution --- exercises/16_lifetimes/lifetimes1.rs | 18 ++++++++++++----- rustlings-macros/info.toml | 3 +-- solutions/16_lifetimes/lifetimes1.rs | 29 +++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/exercises/16_lifetimes/lifetimes1.rs b/exercises/16_lifetimes/lifetimes1.rs index d34f3abd..19e2d398 100644 --- a/exercises/16_lifetimes/lifetimes1.rs +++ b/exercises/16_lifetimes/lifetimes1.rs @@ -3,6 +3,7 @@ // going out of scope before it is used. Remember, references are borrows and do // not own their own data. What if their owner goes out of scope? +// TODO: Fix the compiler error by updating the function signature. fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x @@ -12,9 +13,16 @@ fn longest(x: &str, y: &str) -> &str { } fn main() { - let string1 = String::from("abcd"); - let string2 = "xyz"; - - let result = longest(string1.as_str(), string2); - println!("The longest string is '{}'", result); + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_longest() { + assert_eq!(longest("abcd", "123"), "abcd"); + assert_eq!(longest("abc", "1234"), "1234"); + } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index bed2eafa..e2ebfa5c 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -821,9 +821,8 @@ You may need this: [[exercises]] name = "lifetimes1" dir = "16_lifetimes" -test = false hint = """ -Let the compiler guide you. Also take a look at the book if you need help: +Let the compiler guide you. Also take a look at The Book if you need help: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html""" [[exercises]] diff --git a/solutions/16_lifetimes/lifetimes1.rs b/solutions/16_lifetimes/lifetimes1.rs index 4e181989..ca7b688d 100644 --- a/solutions/16_lifetimes/lifetimes1.rs +++ b/solutions/16_lifetimes/lifetimes1.rs @@ -1 +1,28 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The Rust compiler needs to know how to check whether supplied references are +// valid, so that it can let the programmer know if a reference is at risk of +// going out of scope before it is used. Remember, references are borrows and do +// not own their own data. What if their owner goes out of scope? + +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + // ^^^^ ^^ ^^ ^^ + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_longest() { + assert_eq!(longest("abcd", "123"), "abcd"); + assert_eq!(longest("abc", "1234"), "1234"); + } +} From 275a854d6ec71e4cdde9b4d1943a4dd6e3368ab6 Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 13:24:27 +0200 Subject: [PATCH 392/433] lifetimes2 solution --- exercises/16_lifetimes/lifetimes2.rs | 10 ++++---- solutions/16_lifetimes/lifetimes2.rs | 34 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/exercises/16_lifetimes/lifetimes2.rs b/exercises/16_lifetimes/lifetimes2.rs index 6e329e6d..de5a5dfc 100644 --- a/exercises/16_lifetimes/lifetimes2.rs +++ b/exercises/16_lifetimes/lifetimes2.rs @@ -1,6 +1,4 @@ -// So if the compiler is just validating the references passed to the annotated -// parameters and the return type, what do we need to change? - +// Don't change this function. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x @@ -10,11 +8,13 @@ fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { } fn main() { + // TODO: Fix the compiler error by moving one line. + let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); - result = longest(string1.as_str(), string2.as_str()); + result = longest(&string1, &string2); } - println!("The longest string is '{}'", result); + println!("The longest string is '{result}'"); } diff --git a/solutions/16_lifetimes/lifetimes2.rs b/solutions/16_lifetimes/lifetimes2.rs index 4e181989..b0f2ef1f 100644 --- a/solutions/16_lifetimes/lifetimes2.rs +++ b/solutions/16_lifetimes/lifetimes2.rs @@ -1 +1,33 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let string1 = String::from("long string is long"); + // Solution1: You can move `strings2` out of the inner block so that it is + // not dropped before the print statement. + let string2 = String::from("xyz"); + let result; + { + result = longest(&string1, &string2); + } + println!("The longest string is '{result}'"); + // `string2` dropped at the end of the function. + + // ========================================================================= + + let string1 = String::from("long string is long"); + let result; + { + let string2 = String::from("xyz"); + result = longest(&string1, &string2); + // Solution2: You can move the print statement into the inner block so + // that it is executed before `string2` is dropped. + println!("The longest string is '{result}'"); + // `string2` dropped here (end of the inner scope). + } +} From 61872166067ab83fbad87e55c562e28d98368bff Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 16:15:53 +0200 Subject: [PATCH 393/433] lifetimes3 solution --- exercises/16_lifetimes/lifetimes3.rs | 7 +++---- rustlings-macros/info.toml | 4 +--- solutions/16_lifetimes/lifetimes3.rs | 19 ++++++++++++++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/exercises/16_lifetimes/lifetimes3.rs b/exercises/16_lifetimes/lifetimes3.rs index 9b631cab..1cc27592 100644 --- a/exercises/16_lifetimes/lifetimes3.rs +++ b/exercises/16_lifetimes/lifetimes3.rs @@ -1,16 +1,15 @@ // Lifetimes are also needed when structs hold references. +// TODO: Fix the compiler errors about the struct. struct Book { author: &str, title: &str, } fn main() { - let name = String::from("Jill Smith"); - let title = String::from("Fish Flying"); let book = Book { - author: &name, - title: &title, + author: "George Orwell", + title: "1984", }; println!("{} by {}", book.title, book.author); diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index e2ebfa5c..f0e34a5e 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -843,9 +843,7 @@ inner block: name = "lifetimes3" dir = "16_lifetimes" test = false -hint = """ -If you use a lifetime annotation in a struct's fields, where else does it need -to be added?""" +hint = """Let the compiler guide you :)""" # TESTS diff --git a/solutions/16_lifetimes/lifetimes3.rs b/solutions/16_lifetimes/lifetimes3.rs index 4e181989..16a5a684 100644 --- a/solutions/16_lifetimes/lifetimes3.rs +++ b/solutions/16_lifetimes/lifetimes3.rs @@ -1 +1,18 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Lifetimes are also needed when structs hold references. + +struct Book<'a> { + // ^^^^ added a lifetime annotation + author: &'a str, + // ^^ + title: &'a str, + // ^^ +} + +fn main() { + let book = Book { + author: "George Orwell", + title: "1984", + }; + + println!("{} by {}", book.title, book.author); +} From a4f8826301c793180d94e891603fab22e9492f5c Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 16:29:03 +0200 Subject: [PATCH 394/433] tests1 solution --- exercises/17_tests/tests1.rs | 15 ++++++++++----- rustlings-macros/info.toml | 3 --- solutions/17_tests/tests1.rs | 25 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/exercises/17_tests/tests1.rs b/exercises/17_tests/tests1.rs index 854a1358..7529f9f0 100644 --- a/exercises/17_tests/tests1.rs +++ b/exercises/17_tests/tests1.rs @@ -1,9 +1,9 @@ // Tests are important to ensure that your code does what you think it should -// do. Tests can be run on this file with the following command: rustlings run -// tests1 -// -// This test has a problem with it -- make the test compile! Make the test pass! -// Make the test fail! +// do. + +fn is_even(n: i64) -> bool { + n % 2 == 0 +} fn main() { // You can optionally experiment here. @@ -11,8 +11,13 @@ fn main() { #[cfg(test)] mod tests { + // TODO: Import `is_even`. You can use a wildcard to import everything in + // the outer module. + #[test] fn you_can_assert() { + // TODO: Test the function `is_even` with some values. + assert!(); assert!(); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index f0e34a5e..27ed6b9c 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -851,9 +851,6 @@ hint = """Let the compiler guide you :)""" name = "tests1" dir = "17_tests" hint = """ -You don't even need to write any code to test -- you can just test values and -run that, even though you wouldn't do that in real life. :) - `assert!` is a macro that needs an argument. Depending on the value of the argument, `assert!` will do nothing (in which case the test will pass) or `assert!` will panic (in which case the test will fail). diff --git a/solutions/17_tests/tests1.rs b/solutions/17_tests/tests1.rs index 4e181989..c52b8b16 100644 --- a/solutions/17_tests/tests1.rs +++ b/solutions/17_tests/tests1.rs @@ -1 +1,24 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Tests are important to ensure that your code does what you think it should +// do. + +fn is_even(n: i64) -> bool { + n % 2 == 0 +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + // When writing unit tests, it is common to import everything from the outer + // module (`super`) using a wildcard. + use super::*; + + #[test] + fn you_can_assert() { + assert!(is_even(0)); + assert!(!is_even(-1)); + // ^ You can assert `false` using the negation operator `!`. + } +} From 803e32dad2395d309b74b9fde6b9e08577cf8a0a Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 16:40:26 +0200 Subject: [PATCH 395/433] tests2 solution --- exercises/17_tests/tests2.rs | 13 +++++++++++-- rustlings-macros/info.toml | 6 +----- solutions/17_tests/tests2.rs | 23 ++++++++++++++++++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/exercises/17_tests/tests2.rs b/exercises/17_tests/tests2.rs index f0899e1b..0c6573e8 100644 --- a/exercises/17_tests/tests2.rs +++ b/exercises/17_tests/tests2.rs @@ -1,5 +1,8 @@ -// This test has a problem with it -- make the test compile! Make the test pass! -// Make the test fail! +// Calculates the power of 2 using a bit shift. +// `1 << n` is equivalent to "2 to the power of n". +fn power_of_2(n: u8) -> u64 { + 1 << n +} fn main() { // You can optionally experiment here. @@ -7,8 +10,14 @@ fn main() { #[cfg(test)] mod tests { + use super::*; + #[test] fn you_can_assert_eq() { + // TODO: Test the function `power_of_2` with some values. + assert_eq!(); + assert_eq!(); + assert_eq!(); assert_eq!(); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 27ed6b9c..4fd2bd85 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -862,13 +862,9 @@ ones pass, and which ones fail :)""" name = "tests2" dir = "17_tests" hint = """ -Like the previous exercise, you don't need to write any code to get this test -to compile and run. - `assert_eq!` is a macro that takes two arguments and compares them. Try giving it two values that are equal! Try giving it two arguments that are different! -Try giving it two values that are of different types! Try switching which -argument comes first and which comes second!""" +Try switching which argument comes first and which comes second!""" [[exercises]] name = "tests3" diff --git a/solutions/17_tests/tests2.rs b/solutions/17_tests/tests2.rs index 4e181989..39a0005e 100644 --- a/solutions/17_tests/tests2.rs +++ b/solutions/17_tests/tests2.rs @@ -1 +1,22 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Calculates the power of 2 using a bit shift. +// `1 << n` is equivalent to "2 to the power of n". +fn power_of_2(n: u8) -> u64 { + 1 << n +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn you_can_assert_eq() { + assert_eq!(power_of_2(0), 1); + assert_eq!(power_of_2(1), 2); + assert_eq!(power_of_2(2), 4); + assert_eq!(power_of_2(3), 8); + } +} From 746cf6863dee8f676596b07e74bad1a19fa2579e Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 27 Jun 2024 17:29:33 +0200 Subject: [PATCH 396/433] Remove tests3 and add solution to tests4 --- dev/Cargo.toml | 2 -- exercises/17_tests/tests3.rs | 41 +++++++++++++++++++++++++------- exercises/17_tests/tests4.rs | 45 ----------------------------------- rustlings-macros/info.toml | 19 +++++---------- solutions/17_tests/tests3.rs | 46 +++++++++++++++++++++++++++++++++++- solutions/17_tests/tests4.rs | 1 - 6 files changed, 83 insertions(+), 71 deletions(-) delete mode 100644 exercises/17_tests/tests4.rs delete mode 100644 solutions/17_tests/tests4.rs diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 2c5eaf09..7f3acb51 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -140,8 +140,6 @@ bin = [ { name = "tests2_sol", path = "../solutions/17_tests/tests2.rs" }, { name = "tests3", path = "../exercises/17_tests/tests3.rs" }, { name = "tests3_sol", path = "../solutions/17_tests/tests3.rs" }, - { name = "tests4", path = "../exercises/17_tests/tests4.rs" }, - { name = "tests4_sol", path = "../solutions/17_tests/tests4.rs" }, { name = "iterators1", path = "../exercises/18_iterators/iterators1.rs" }, { name = "iterators1_sol", path = "../solutions/18_iterators/iterators1.rs" }, { name = "iterators2", path = "../exercises/18_iterators/iterators2.rs" }, diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index d1cb4892..9fc93185 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -1,9 +1,19 @@ -// This test isn't testing our function -- make it do that in such a way that -// the test passes. Then write a second test that tests whether we get the -// result we expect to get when we call `is_even(5)`. +struct Rectangle { + width: i32, + height: i32, +} -fn is_even(num: i32) -> bool { - num % 2 == 0 +impl Rectangle { + // Don't change this function. + fn new(width: i32, height: i32) -> Self { + if width <= 0 || height <= 0 { + // Returning a `Result` would be better here. But we want to learn + // how to test functions that can panic. + panic!("Rectangle width and height can't be negative"); + } + + Rectangle { width, height } + } } fn main() { @@ -15,12 +25,25 @@ mod tests { use super::*; #[test] - fn is_true_when_even() { - assert!(); + fn correct_width_and_height() { + // TODO: This test should check if the rectangle has the size that we + // pass to its constructor. + let rect = Rectangle::new(10, 20); + assert_eq!(???, 10); // Check width + assert_eq!(???, 20); // Check height } + // TODO: This test should check if the program panics when we try to create + // a rectangle with negative width. #[test] - fn is_false_when_odd() { - assert!(); + fn negative_width() { + let _rect = Rectangle::new(-10, 10); + } + + // TODO: This test should check if the program panics when we try to create + // a rectangle with negative height. + #[test] + fn negative_height() { + let _rect = Rectangle::new(10, -10); } } diff --git a/exercises/17_tests/tests4.rs b/exercises/17_tests/tests4.rs deleted file mode 100644 index 4303ed06..00000000 --- a/exercises/17_tests/tests4.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Make sure that we're testing for the correct conditions! - -struct Rectangle { - width: i32, - height: i32, -} - -impl Rectangle { - // Only change the test functions themselves - fn new(width: i32, height: i32) -> Self { - if width <= 0 || height <= 0 { - panic!("Rectangle width and height cannot be negative!") - } - Rectangle { width, height } - } -} - -fn main() { - // You can optionally experiment here. -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn correct_width_and_height() { - // This test should check if the rectangle is the size that we pass into its constructor - let rect = Rectangle::new(10, 20); - assert_eq!(???, 10); // check width - assert_eq!(???, 20); // check height - } - - #[test] - fn negative_width() { - // This test should check if program panics when we try to create rectangle with negative width - let _rect = Rectangle::new(-10, 10); - } - - #[test] - fn negative_height() { - // This test should check if program panics when we try to create rectangle with negative height - let _rect = Rectangle::new(10, -10); - } -} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 4fd2bd85..5c24cd33 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -856,7 +856,10 @@ argument, `assert!` will do nothing (in which case the test will pass) or `assert!` will panic (in which case the test will fail). So try giving different values to `assert!` and see which ones compile, which -ones pass, and which ones fail :)""" +ones pass, and which ones fail :) + +If you want to check for `false`, you can negate the result of what you're +checking using `!`, like `assert!(!…)`.""" [[exercises]] name = "tests2" @@ -870,19 +873,9 @@ Try switching which argument comes first and which comes second!""" name = "tests3" dir = "17_tests" hint = """ -You can call a function right where you're passing arguments to `assert!`. So -you could do something like `assert!(having_fun())`. +We expect the method `Rectangle::new` to panic for negative values. -If you want to check that you indeed get `false`, you can negate the result of -what you're doing using `!`, like `assert!(!having_fun())`.""" - -[[exercises]] -name = "tests4" -dir = "17_tests" -hint = """ -We expect method `Rectangle::new()` to panic for negative values. - -To handle that you need to add a special attribute to the test function. +To handle that, you need to add a special attribute to the test function. You can refer to the docs: https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic""" diff --git a/solutions/17_tests/tests3.rs b/solutions/17_tests/tests3.rs index 4e181989..503f9bc0 100644 --- a/solutions/17_tests/tests3.rs +++ b/solutions/17_tests/tests3.rs @@ -1 +1,45 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +struct Rectangle { + width: i32, + height: i32, +} + +impl Rectangle { + // Don't change this function. + fn new(width: i32, height: i32) -> Self { + if width <= 0 || height <= 0 { + // Returning a `Result` would be better here. But we want to learn + // how to test functions that can panic. + panic!("Rectangle width and height can't be negative"); + } + + Rectangle { width, height } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn correct_width_and_height() { + let rect = Rectangle::new(10, 20); + assert_eq!(rect.width, 10); // Check width + assert_eq!(rect.height, 20); // Check height + } + + #[test] + #[should_panic] // Added this attribute to check that the test panics. + fn negative_width() { + let _rect = Rectangle::new(-10, 10); + } + + #[test] + #[should_panic] // Added this attribute to check that the test panics. + fn negative_height() { + let _rect = Rectangle::new(10, -10); + } +} diff --git a/solutions/17_tests/tests4.rs b/solutions/17_tests/tests4.rs deleted file mode 100644 index 4e181989..00000000 --- a/solutions/17_tests/tests4.rs +++ /dev/null @@ -1 +0,0 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 From cf9041c0e42120199e09e74e65c52d69c00db19c Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 02:07:56 +0200 Subject: [PATCH 397/433] iterators1 solution --- exercises/18_iterators/iterators1.rs | 21 +++++++++------------ rustlings-macros/info.toml | 15 +-------------- solutions/18_iterators/iterators1.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index 52b704d5..86278a49 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -1,8 +1,6 @@ // When performing operations on elements within a collection, iterators are // essential. This module helps you get familiar with the structure of using an // iterator and how to go through elements within an iterable collection. -// -// Make me compile by filling in the `???`s fn main() { // You can optionally experiment here. @@ -10,19 +8,18 @@ fn main() { #[cfg(test)] mod tests { - use super::*; - #[test] fn iterators() { - let my_fav_fruits = vec!["banana", "custard apple", "avocado", "peach", "raspberry"]; + let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; - let mut my_iterable_fav_fruits = ???; // TODO: Step 1 + // TODO: Create an iterator over the array. + let mut fav_fruits_iterator = ???; - assert_eq!(my_iterable_fav_fruits.next(), Some(&"banana")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 2 - assert_eq!(my_iterable_fav_fruits.next(), Some(&"avocado")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 3 - assert_eq!(my_iterable_fav_fruits.next(), Some(&"raspberry")); - assert_eq!(my_iterable_fav_fruits.next(), ???); // TODO: Step 4 + assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); + assert_eq!(fav_fruits_iterator.next(), ???); // TODO: Replace `???` + assert_eq!(fav_fruits_iterator.next(), Some(&"avocado")); + assert_eq!(fav_fruits_iterator.next(), ???); // TODO: Replace `???` + assert_eq!(fav_fruits_iterator.next(), Some(&"raspberry")); + assert_eq!(fav_fruits_iterator.next(), ???); // TODO: Replace `???` } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 5c24cd33..5e939867 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -886,22 +886,9 @@ https://doc.rust-lang.org/stable/book/ch11-01-writing-tests.html#checking-for-pa name = "iterators1" dir = "18_iterators" hint = """ -Step 1: - -We need to apply something to the collection `my_fav_fruits` before we start to -go through it. What could that be? Take a look at the struct definition for a -vector for inspiration: -https://doc.rust-lang.org/std/vec/struct.Vec.html - -Step 2 & step 3: - -Very similar to the lines above and below. You've got this! - -Step 4: - An iterator goes through all elements in a collection, but what if we've run out of elements? What should we expect here? If you're stuck, take a look at -https://doc.rust-lang.org/std/iter/trait.Iterator.html for some ideas.""" +https://doc.rust-lang.org/std/iter/trait.Iterator.html""" [[exercises]] name = "iterators2" diff --git a/solutions/18_iterators/iterators1.rs b/solutions/18_iterators/iterators1.rs index 4e181989..93a6008a 100644 --- a/solutions/18_iterators/iterators1.rs +++ b/solutions/18_iterators/iterators1.rs @@ -1 +1,26 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// When performing operations on elements within a collection, iterators are +// essential. This module helps you get familiar with the structure of using an +// iterator and how to go through elements within an iterable collection. + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + #[test] + fn iterators() { + let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; + + // Create an iterator over the array. + let mut fav_fruits_iterator = my_fav_fruits.iter(); + + assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); + assert_eq!(fav_fruits_iterator.next(), Some(&"custard apple")); + assert_eq!(fav_fruits_iterator.next(), Some(&"avocado")); + assert_eq!(fav_fruits_iterator.next(), Some(&"peach")); + assert_eq!(fav_fruits_iterator.next(), Some(&"raspberry")); + assert_eq!(fav_fruits_iterator.next(), None); + // ^^^^ reached the end + } +} From 4f71f74b444ab35e0de0c4bd9a01a7e438057c01 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 02:26:35 +0200 Subject: [PATCH 398/433] Use todo!() instead of ??? --- exercises/17_tests/tests3.rs | 4 ++-- exercises/18_iterators/iterators1.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exercises/17_tests/tests3.rs b/exercises/17_tests/tests3.rs index 9fc93185..ec994792 100644 --- a/exercises/17_tests/tests3.rs +++ b/exercises/17_tests/tests3.rs @@ -29,8 +29,8 @@ mod tests { // TODO: This test should check if the rectangle has the size that we // pass to its constructor. let rect = Rectangle::new(10, 20); - assert_eq!(???, 10); // Check width - assert_eq!(???, 20); // Check height + assert_eq!(todo!(), 10); // Check width + assert_eq!(todo!(), 20); // Check height } // TODO: This test should check if the program panics when we try to create diff --git a/exercises/18_iterators/iterators1.rs b/exercises/18_iterators/iterators1.rs index 86278a49..ca937ed0 100644 --- a/exercises/18_iterators/iterators1.rs +++ b/exercises/18_iterators/iterators1.rs @@ -13,13 +13,13 @@ mod tests { let my_fav_fruits = ["banana", "custard apple", "avocado", "peach", "raspberry"]; // TODO: Create an iterator over the array. - let mut fav_fruits_iterator = ???; + let mut fav_fruits_iterator = todo!(); assert_eq!(fav_fruits_iterator.next(), Some(&"banana")); - assert_eq!(fav_fruits_iterator.next(), ???); // TODO: Replace `???` + assert_eq!(fav_fruits_iterator.next(), todo!()); // TODO: Replace `todo!()` assert_eq!(fav_fruits_iterator.next(), Some(&"avocado")); - assert_eq!(fav_fruits_iterator.next(), ???); // TODO: Replace `???` + assert_eq!(fav_fruits_iterator.next(), todo!()); // TODO: Replace `todo!()` assert_eq!(fav_fruits_iterator.next(), Some(&"raspberry")); - assert_eq!(fav_fruits_iterator.next(), ???); // TODO: Replace `???` + assert_eq!(fav_fruits_iterator.next(), todo!()); // TODO: Replace `todo!()` } } From eddbb97934b8d358b4fd20cc3063cf4872e39567 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 02:48:21 +0200 Subject: [PATCH 399/433] iterators2 solution --- exercises/18_iterators/iterators2.rs | 23 +++++------ rustlings-macros/info.toml | 9 +++-- solutions/18_iterators/iterators2.rs | 57 +++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/exercises/18_iterators/iterators2.rs b/exercises/18_iterators/iterators2.rs index 8d8909bf..5903e657 100644 --- a/exercises/18_iterators/iterators2.rs +++ b/exercises/18_iterators/iterators2.rs @@ -1,31 +1,28 @@ // In this exercise, you'll learn some of the unique advantages that iterators -// can offer. Follow the steps to complete the exercise. +// can offer. -// Step 1. -// Complete the `capitalize_first` function. +// TODO: Complete the `capitalize_first` function. // "hello" -> "Hello" fn capitalize_first(input: &str) -> String { - let mut c = input.chars(); - match c.next() { + let mut chars = input.chars(); + match chars.next() { None => String::new(), - Some(first) => ???, + Some(first) => todo!(), } } -// Step 2. -// Apply the `capitalize_first` function to a slice of string slices. +// TODO: Apply the `capitalize_first` function to a slice of string slices. // Return a vector of strings. // ["hello", "world"] -> ["Hello", "World"] fn capitalize_words_vector(words: &[&str]) -> Vec { - vec![] + // ??? } -// Step 3. -// Apply the `capitalize_first` function again to a slice of string slices. -// Return a single string. +// TODO: Apply the `capitalize_first` function again to a slice of string +// slices. Return a single string. // ["hello", " ", "world"] -> "Hello World" fn capitalize_words_string(words: &[&str]) -> String { - String::new() + // ??? } fn main() { diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 5e939867..5a337888 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -894,7 +894,7 @@ https://doc.rust-lang.org/std/iter/trait.Iterator.html""" name = "iterators2" dir = "18_iterators" hint = """ -Step 1: +`capitalize_first`: The variable `first` is a `char`. It needs to be capitalized and added to the remaining characters in `c` in order to return the correct `String`. @@ -905,12 +905,15 @@ The remaining characters in `c` can be viewed as a string slice using the The documentation for `char` contains many useful methods. https://doc.rust-lang.org/std/primitive.char.html -Step 2: +Use `char::to_uppercase`. It returns an iterator that can be converted to a +`String`. + +`capitalize_words_vector`: Create an iterator from the slice. Transform the iterated values by applying the `capitalize_first` function. Remember to `collect` the iterator. -Step 3: +`capitalize_words_string`: This is surprisingly similar to the previous solution. `collect` is very powerful and very general. Rust just needs to know the desired type.""" diff --git a/solutions/18_iterators/iterators2.rs b/solutions/18_iterators/iterators2.rs index 4e181989..db05f293 100644 --- a/solutions/18_iterators/iterators2.rs +++ b/solutions/18_iterators/iterators2.rs @@ -1 +1,56 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// In this exercise, you'll learn some of the unique advantages that iterators +// can offer. + +// "hello" -> "Hello" +fn capitalize_first(input: &str) -> String { + let mut chars = input.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().to_string() + chars.as_str(), + } +} + +// Apply the `capitalize_first` function to a slice of string slices. +// Return a vector of strings. +// ["hello", "world"] -> ["Hello", "World"] +fn capitalize_words_vector(words: &[&str]) -> Vec { + words.iter().map(|word| capitalize_first(word)).collect() +} + +// Apply the `capitalize_first` function again to a slice of string +// slices. Return a single string. +// ["hello", " ", "world"] -> "Hello World" +fn capitalize_words_string(words: &[&str]) -> String { + words.iter().map(|word| capitalize_first(word)).collect() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_success() { + assert_eq!(capitalize_first("hello"), "Hello"); + } + + #[test] + fn test_empty() { + assert_eq!(capitalize_first(""), ""); + } + + #[test] + fn test_iterate_string_vec() { + let words = vec!["hello", "world"]; + assert_eq!(capitalize_words_vector(&words), ["Hello", "World"]); + } + + #[test] + fn test_iterate_into_string() { + let words = vec!["hello", " ", "world"]; + assert_eq!(capitalize_words_string(&words), "Hello World"); + } +} From 56a9197f55356a0a6503d6fa6cb2241d676bd051 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 15:00:13 +0200 Subject: [PATCH 400/433] iterators3 solution --- exercises/18_iterators/iterators3.rs | 55 ++++++--------------- rustlings-macros/info.toml | 6 +-- solutions/18_iterators/iterators3.rs | 74 +++++++++++++++++++++++++++- 3 files changed, 92 insertions(+), 43 deletions(-) diff --git a/exercises/18_iterators/iterators3.rs b/exercises/18_iterators/iterators3.rs index dfe40149..b5d05f6e 100644 --- a/exercises/18_iterators/iterators3.rs +++ b/exercises/18_iterators/iterators3.rs @@ -1,40 +1,26 @@ -// This is a bigger exercise than most of the others! You can do it! Here is -// your mission, should you choose to accept it: -// 1. Complete the divide function to get the first four tests to pass. -// 2. Get the remaining tests to pass by completing the result_with_list and -// list_of_results functions. - #[derive(Debug, PartialEq, Eq)] enum DivisionError { - NotDivisible(NotDivisibleError), DivideByZero, + NotDivisible, } -#[derive(Debug, PartialEq, Eq)] -struct NotDivisibleError { - dividend: i32, - divisor: i32, -} - -// Calculate `a` divided by `b` if `a` is evenly divisible by `b`. +// TODO: Calculate `a` divided by `b` if `a` is evenly divisible by `b`. // Otherwise, return a suitable error. fn divide(a: i32, b: i32) -> Result { todo!(); } -// Complete the function and return a value of the correct type so the test -// passes. -// Desired output: Ok([1, 11, 1426, 3]) -fn result_with_list() -> () { - let numbers = vec![27, 297, 38502, 81]; +// TODO: Add the correct return type and complete the function body. +// Desired output: `Ok([1, 11, 1426, 3])` +fn result_with_list() { + let numbers = [27, 297, 38502, 81]; let division_results = numbers.into_iter().map(|n| divide(n, 27)); } -// Complete the function and return a value of the correct type so the test -// passes. -// Desired output: [Ok(1), Ok(11), Ok(1426), Ok(3)] -fn list_of_results() -> () { - let numbers = vec![27, 297, 38502, 81]; +// TODO: Add the correct return type and complete the function body. +// Desired output: `[Ok(1), Ok(11), Ok(1426), Ok(3)]` +fn list_of_results() { + let numbers = [27, 297, 38502, 81]; let division_results = numbers.into_iter().map(|n| divide(n, 27)); } @@ -52,19 +38,13 @@ mod tests { } #[test] - fn test_not_divisible() { - assert_eq!( - divide(81, 6), - Err(DivisionError::NotDivisible(NotDivisibleError { - dividend: 81, - divisor: 6 - })) - ); + fn test_divide_by_0() { + assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero)); } #[test] - fn test_divide_by_0() { - assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero)); + fn test_not_divisible() { + assert_eq!(divide(81, 6), Err(DivisionError::NotDivisible)); } #[test] @@ -74,14 +54,11 @@ mod tests { #[test] fn test_result_with_list() { - assert_eq!(format!("{:?}", result_with_list()), "Ok([1, 11, 1426, 3])"); + assert_eq!(result_with_list().unwarp(), [1, 11, 1426, 3]); } #[test] fn test_list_of_results() { - assert_eq!( - format!("{:?}", list_of_results()), - "[Ok(1), Ok(11), Ok(1426), Ok(3)]" - ); + assert_eq!(list_of_results(), [Ok(1), Ok(11), Ok(1426), Ok(3)]); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 5a337888..8b1feb49 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -922,8 +922,8 @@ powerful and very general. Rust just needs to know the desired type.""" name = "iterators3" dir = "18_iterators" hint = """ -The `divide` function needs to return the correct error when even division is -not possible. +The `divide` function needs to return the correct error when the divisor is 0 or +when even division is not possible. The `division_results` variable needs to be collected into a collection type. @@ -934,7 +934,7 @@ The `list_of_results` function needs to return a vector of results. See https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect for how the `FromIterator` trait is used in `collect()`. This trait is REALLY -powerful! It can make the solution to this exercise infinitely easier.""" +powerful! It can make the solution to this exercise much easier.""" [[exercises]] name = "iterators4" diff --git a/solutions/18_iterators/iterators3.rs b/solutions/18_iterators/iterators3.rs index 4e181989..d66d1ef3 100644 --- a/solutions/18_iterators/iterators3.rs +++ b/solutions/18_iterators/iterators3.rs @@ -1 +1,73 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +#[derive(Debug, PartialEq, Eq)] +enum DivisionError { + DivideByZero, + NotDivisible, +} + +fn divide(a: i64, b: i64) -> Result { + if b == 0 { + return Err(DivisionError::DivideByZero); + } + + if a % b != 0 { + return Err(DivisionError::NotDivisible); + } + + Ok(a / b) +} + +fn result_with_list() -> Result, DivisionError> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let numbers = [27, 297, 38502, 81]; + let division_results = numbers.into_iter().map(|n| divide(n, 27)); + // Collects to the expected return type. Returns the first error in the + // division results (if one exists). + division_results.collect() +} + +fn list_of_results() -> Vec> { + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + let numbers = [27, 297, 38502, 81]; + let division_results = numbers.into_iter().map(|n| divide(n, 27)); + // Collects to the expected return type. + division_results.collect() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_success() { + assert_eq!(divide(81, 9), Ok(9)); + } + + #[test] + fn test_divide_by_0() { + assert_eq!(divide(81, 0), Err(DivisionError::DivideByZero)); + } + + #[test] + fn test_not_divisible() { + assert_eq!(divide(81, 6), Err(DivisionError::NotDivisible)); + } + + #[test] + fn test_divide_0_by_something() { + assert_eq!(divide(0, 81), Ok(0)); + } + + #[test] + fn test_result_with_list() { + assert_eq!(result_with_list().unwrap(), [1, 11, 1426, 3]); + } + + #[test] + fn test_list_of_results() { + assert_eq!(list_of_results(), [Ok(1), Ok(11), Ok(1426), Ok(3)]); + } +} From 2af437fd901345f2613217cbf325718672d04100 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 15:31:15 +0200 Subject: [PATCH 401/433] iterators4 solution --- exercises/18_iterators/iterators4.rs | 14 +++--- rustlings-macros/info.toml | 4 +- solutions/18_iterators/iterators4.rs | 72 +++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/exercises/18_iterators/iterators4.rs b/exercises/18_iterators/iterators4.rs index ae4d502d..08ba3650 100644 --- a/exercises/18_iterators/iterators4.rs +++ b/exercises/18_iterators/iterators4.rs @@ -1,9 +1,9 @@ -fn factorial(num: u64) -> u64 { - // Complete this function to return the factorial of num +fn factorial(num: u8) -> u64 { + // TODO: Complete this function to return the factorial of `num`. // Do not use: // - early returns (using the `return` keyword explicitly) // Try not to use: - // - imperative style loops (for, while) + // - imperative style loops (for/while) // - additional variables // For an extra challenge, don't use: // - recursion @@ -19,20 +19,20 @@ mod tests { #[test] fn factorial_of_0() { - assert_eq!(1, factorial(0)); + assert_eq!(factorial(0), 1); } #[test] fn factorial_of_1() { - assert_eq!(1, factorial(1)); + assert_eq!(factorial(1), 1); } #[test] fn factorial_of_2() { - assert_eq!(2, factorial(2)); + assert_eq!(factorial(2), 2); } #[test] fn factorial_of_4() { - assert_eq!(24, factorial(4)); + assert_eq!(factorial(4), 24); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 8b1feb49..72f956bf 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -942,10 +942,10 @@ dir = "18_iterators" hint = """ In an imperative language, you might write a `for` loop that updates a mutable variable. Or, you might write code utilizing recursion and a match clause. In -Rust you can take another functional approach, computing the factorial +Rust, you can take another functional approach, computing the factorial elegantly with ranges and iterators. -Hint 2: Check out the `fold` and `rfold` methods!""" +Check out the `fold` and `rfold` methods!""" [[exercises]] name = "iterators5" diff --git a/solutions/18_iterators/iterators4.rs b/solutions/18_iterators/iterators4.rs index 4e181989..4c3c49d9 100644 --- a/solutions/18_iterators/iterators4.rs +++ b/solutions/18_iterators/iterators4.rs @@ -1 +1,71 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// 3 possible solutions are presented. + +// With `for` loop and a mutable variable. +fn factorial_for(num: u64) -> u64 { + let mut result = 1; + + for x in 2..=num { + result *= x; + } + + result +} + +// Equivalent to `factorial_for` but shorter and without a `for` loop and +// mutable variables. +fn factorial_fold(num: u64) -> u64 { + // Case num==0: The iterator 2..=0 is empty + // -> The initial value of `fold` is returned which is 1. + // Case num==1: The iterator 2..=1 is also empty + // -> The initial value 1 is returned. + // Case num==2: The iterator 2..=2 contains one element + // -> The initial value 1 is multiplied by 2 and the result + // is returned. + // Case num==3: The iterator 2..=3 contains 2 elements + // -> 1 * 2 is calculated, then the result 2 is multiplied by + // the second element 3 so the result 6 is returned. + // And so on… + (2..=num).fold(1, |acc, x| acc * x) +} + +// Equivalent to `factorial_fold` but with a built-in method that is suggested +// by Clippy. +fn factorial_product(num: u64) -> u64 { + (2..=num).product() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn factorial_of_0() { + assert_eq!(factorial_for(0), 1); + assert_eq!(factorial_fold(0), 1); + assert_eq!(factorial_product(0), 1); + } + + #[test] + fn factorial_of_1() { + assert_eq!(factorial_for(1), 1); + assert_eq!(factorial_fold(1), 1); + assert_eq!(factorial_product(1), 1); + } + #[test] + fn factorial_of_2() { + assert_eq!(factorial_for(2), 2); + assert_eq!(factorial_fold(2), 2); + assert_eq!(factorial_product(2), 2); + } + + #[test] + fn factorial_of_4() { + assert_eq!(factorial_for(4), 24); + assert_eq!(factorial_fold(4), 24); + assert_eq!(factorial_product(4), 24); + } +} From f53d4589205a5485011a341400eeea0ec3d6b339 Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 16:11:14 +0200 Subject: [PATCH 402/433] iterators5 solution --- exercises/18_iterators/iterators5.rs | 154 +++++++++++++-------------- solutions/18_iterators/iterators5.rs | 151 +++++++++++++++++++++++++- 2 files changed, 227 insertions(+), 78 deletions(-) diff --git a/exercises/18_iterators/iterators5.rs b/exercises/18_iterators/iterators5.rs index 4f052d51..7e434cc5 100644 --- a/exercises/18_iterators/iterators5.rs +++ b/exercises/18_iterators/iterators5.rs @@ -1,10 +1,8 @@ -// Let's define a simple model to track Rustlings exercise progress. Progress +// Let's define a simple model to track Rustlings' exercise progress. Progress // will be modelled using a hash map. The name of the exercise is the key and // the progress is the value. Two counting functions were created to count the // number of exercises with a given progress. Recreate this counting -// functionality using iterators. Try not to use imperative loops (for, while). -// Only the two iterator methods (count_iterator and count_collection_iterator) -// need to be modified. +// functionality using iterators. Try to not use imperative loops (for/while). use std::collections::HashMap; @@ -18,24 +16,25 @@ enum Progress { fn count_for(map: &HashMap, value: Progress) -> usize { let mut count = 0; for val in map.values() { - if val == &value { + if *val == value { count += 1; } } count } +// TODO: Implement the functionality of `count_for` but with an iterator instead +// of a `for` loop. fn count_iterator(map: &HashMap, value: Progress) -> usize { - // map is a hashmap with String keys and Progress values. - // map = { "variables1": Complete, "from_str": None, ... } - todo!(); + // `map` is a hash map with `String` keys and `Progress` values. + // map = { "variables1": Complete, "from_str": None, … } } fn count_collection_for(collection: &[HashMap], value: Progress) -> usize { let mut count = 0; for map in collection { for val in map.values() { - if val == &value { + if *val == value { count += 1; } } @@ -43,11 +42,12 @@ fn count_collection_for(collection: &[HashMap], value: Progres count } +// TODO: Implement the functionality of `count_collection_for` but with an +// iterator instead of a `for` loop. fn count_collection_iterator(collection: &[HashMap], value: Progress) -> usize { - // collection is a slice of hashmaps. - // collection = [{ "variables1": Complete, "from_str": None, ... }, - // { "variables2": Complete, ... }, ... ] - todo!(); + // `collection` is a slice of hash maps. + // collection = [{ "variables1": Complete, "from_str": None, … }, + // { "variables2": Complete, … }, … ] } fn main() { @@ -58,70 +58,6 @@ fn main() { mod tests { use super::*; - #[test] - fn count_complete() { - let map = get_map(); - assert_eq!(3, count_iterator(&map, Progress::Complete)); - } - - #[test] - fn count_some() { - let map = get_map(); - assert_eq!(1, count_iterator(&map, Progress::Some)); - } - - #[test] - fn count_none() { - let map = get_map(); - assert_eq!(2, count_iterator(&map, Progress::None)); - } - - #[test] - fn count_complete_equals_for() { - let map = get_map(); - let progress_states = vec![Progress::Complete, Progress::Some, Progress::None]; - for progress_state in progress_states { - assert_eq!( - count_for(&map, progress_state), - count_iterator(&map, progress_state) - ); - } - } - - #[test] - fn count_collection_complete() { - let collection = get_vec_map(); - assert_eq!( - 6, - count_collection_iterator(&collection, Progress::Complete) - ); - } - - #[test] - fn count_collection_some() { - let collection = get_vec_map(); - assert_eq!(1, count_collection_iterator(&collection, Progress::Some)); - } - - #[test] - fn count_collection_none() { - let collection = get_vec_map(); - assert_eq!(4, count_collection_iterator(&collection, Progress::None)); - } - - #[test] - fn count_collection_equals_for() { - let progress_states = vec![Progress::Complete, Progress::Some, Progress::None]; - let collection = get_vec_map(); - - for progress_state in progress_states { - assert_eq!( - count_collection_for(&collection, progress_state), - count_collection_iterator(&collection, progress_state) - ); - } - } - fn get_map() -> HashMap { use Progress::*; @@ -150,4 +86,68 @@ mod tests { vec![map, other] } + + #[test] + fn count_complete() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Complete), 3); + } + + #[test] + fn count_some() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Some), 1); + } + + #[test] + fn count_none() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::None), 2); + } + + #[test] + fn count_complete_equals_for() { + let map = get_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + for progress_state in progress_states { + assert_eq!( + count_for(&map, progress_state), + count_iterator(&map, progress_state), + ); + } + } + + #[test] + fn count_collection_complete() { + let collection = get_vec_map(); + assert_eq!( + count_collection_iterator(&collection, Progress::Complete), + 6, + ); + } + + #[test] + fn count_collection_some() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::Some), 1); + } + + #[test] + fn count_collection_none() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::None), 4); + } + + #[test] + fn count_collection_equals_for() { + let collection = get_vec_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + + for progress_state in progress_states { + assert_eq!( + count_collection_for(&collection, progress_state), + count_collection_iterator(&collection, progress_state), + ); + } + } } diff --git a/solutions/18_iterators/iterators5.rs b/solutions/18_iterators/iterators5.rs index 4e181989..402c81b0 100644 --- a/solutions/18_iterators/iterators5.rs +++ b/solutions/18_iterators/iterators5.rs @@ -1 +1,150 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Let's define a simple model to track Rustlings' exercise progress. Progress +// will be modelled using a hash map. The name of the exercise is the key and +// the progress is the value. Two counting functions were created to count the +// number of exercises with a given progress. Recreate this counting +// functionality using iterators. Try to not use imperative loops (for/while). + +use std::collections::HashMap; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Progress { + None, + Some, + Complete, +} + +fn count_for(map: &HashMap, value: Progress) -> usize { + let mut count = 0; + for val in map.values() { + if *val == value { + count += 1; + } + } + count +} + +fn count_iterator(map: &HashMap, value: Progress) -> usize { + // `map` is a hash map with `String` keys and `Progress` values. + // map = { "variables1": Complete, "from_str": None, … } + map.values().filter(|val| **val == value).count() +} + +fn count_collection_for(collection: &[HashMap], value: Progress) -> usize { + let mut count = 0; + for map in collection { + count += count_for(map, value); + } + count +} + +fn count_collection_iterator(collection: &[HashMap], value: Progress) -> usize { + // `collection` is a slice of hash maps. + // collection = [{ "variables1": Complete, "from_str": None, … }, + // { "variables2": Complete, … }, … ] + collection + .iter() + .map(|map| count_iterator(map, value)) + .sum() +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + fn get_map() -> HashMap { + use Progress::*; + + let mut map = HashMap::new(); + map.insert(String::from("variables1"), Complete); + map.insert(String::from("functions1"), Complete); + map.insert(String::from("hashmap1"), Complete); + map.insert(String::from("arc1"), Some); + map.insert(String::from("as_ref_mut"), None); + map.insert(String::from("from_str"), None); + + map + } + + fn get_vec_map() -> Vec> { + use Progress::*; + + let map = get_map(); + + let mut other = HashMap::new(); + other.insert(String::from("variables2"), Complete); + other.insert(String::from("functions2"), Complete); + other.insert(String::from("if1"), Complete); + other.insert(String::from("from_into"), None); + other.insert(String::from("try_from_into"), None); + + vec![map, other] + } + + #[test] + fn count_complete() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Complete), 3); + } + + #[test] + fn count_some() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::Some), 1); + } + + #[test] + fn count_none() { + let map = get_map(); + assert_eq!(count_iterator(&map, Progress::None), 2); + } + + #[test] + fn count_complete_equals_for() { + let map = get_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + for progress_state in progress_states { + assert_eq!( + count_for(&map, progress_state), + count_iterator(&map, progress_state), + ); + } + } + + #[test] + fn count_collection_complete() { + let collection = get_vec_map(); + assert_eq!( + count_collection_iterator(&collection, Progress::Complete), + 6, + ); + } + + #[test] + fn count_collection_some() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::Some), 1); + } + + #[test] + fn count_collection_none() { + let collection = get_vec_map(); + assert_eq!(count_collection_iterator(&collection, Progress::None), 4); + } + + #[test] + fn count_collection_equals_for() { + let collection = get_vec_map(); + let progress_states = [Progress::Complete, Progress::Some, Progress::None]; + + for progress_state in progress_states { + assert_eq!( + count_collection_for(&collection, progress_state), + count_collection_iterator(&collection, progress_state), + ); + } + } +} From 61c7eaed6251fb8a28b00ea97b22d1f1b778a72b Mon Sep 17 00:00:00 2001 From: mo8it Date: Fri, 28 Jun 2024 21:24:35 +0200 Subject: [PATCH 403/433] box1 solution --- exercises/19_smart_pointers/box1.rs | 30 +++++++++--------- rustlings-macros/info.toml | 15 +++------ solutions/19_smart_pointers/box1.rs | 48 ++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 27 deletions(-) diff --git a/exercises/19_smart_pointers/box1.rs b/exercises/19_smart_pointers/box1.rs index c8c2640d..d70e1c3d 100644 --- a/exercises/19_smart_pointers/box1.rs +++ b/exercises/19_smart_pointers/box1.rs @@ -4,45 +4,43 @@ // `Box` - a smart pointer used to store data on the heap, which also allows us // to wrap a recursive type. // -// The recursive type we're implementing in this exercise is the `cons list` - a +// The recursive type we're implementing in this exercise is the "cons list", a // data structure frequently found in functional programming languages. Each -// item in a cons list contains two elements: the value of the current item and +// item in a cons list contains two elements: The value of the current item and // the next item. The last item is a value called `Nil`. -// -// Step 1: use a `Box` in the enum definition to make the code compile -// Step 2: create both empty and non-empty cons lists by replacing `todo!()` -// -// Note: the tests should not be changed +// TODO: Use a `Box` in the enum definition to make the code compile. #[derive(PartialEq, Debug)] enum List { Cons(i32, List), Nil, } -fn main() { - println!("This is an empty cons list: {:?}", create_empty_list()); - println!( - "This is a non-empty cons list: {:?}", - create_non_empty_list() - ); -} - +// TODO: Create an empty cons list. fn create_empty_list() -> List { todo!() } +// TODO: Create a non-empty cons list. fn create_non_empty_list() -> List { todo!() } +fn main() { + println!("This is an empty cons list: {:?}", create_empty_list()); + println!( + "This is a non-empty cons list: {:?}", + create_non_empty_list(), + ); +} + #[cfg(test)] mod tests { use super::*; #[test] fn test_create_empty_list() { - assert_eq!(List::Nil, create_empty_list()); + assert_eq!(create_empty_list(), List::Nil); } #[test] diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 72f956bf..744ad089 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -969,21 +969,16 @@ a different method that could make your code more compact than using `fold`.""" name = "box1" dir = "19_smart_pointers" hint = """ -Step 1: - -The compiler's message should help: since we cannot store the value of the +The compiler's message should help: Since we cannot store the value of the actual type when working with recursive types, we need to store a reference (pointer) to its value. -We should, therefore, place our `List` inside a `Box`. More details in the book -here: https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes +We should, therefore, place our `List` inside a `Box`. More details in The Book: +https://doc.rust-lang.org/book/ch15-01-box.html#enabling-recursive-types-with-boxes -Step 2: +Creating an empty list should be fairly straightforward (Hint: Read the tests). -Creating an empty list should be fairly straightforward (hint: peek at the -assertions). - -For a non-empty list keep in mind that we want to use our `Cons` "list builder". +For a non-empty list, keep in mind that we want to use our `Cons` list builder. Although the current list is one of integers (`i32`), feel free to change the definition and try other types!""" diff --git a/solutions/19_smart_pointers/box1.rs b/solutions/19_smart_pointers/box1.rs index 4e181989..189cc562 100644 --- a/solutions/19_smart_pointers/box1.rs +++ b/solutions/19_smart_pointers/box1.rs @@ -1 +1,47 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// At compile time, Rust needs to know how much space a type takes up. This +// becomes problematic for recursive types, where a value can have as part of +// itself another value of the same type. To get around the issue, we can use a +// `Box` - a smart pointer used to store data on the heap, which also allows us +// to wrap a recursive type. +// +// The recursive type we're implementing in this exercise is the "cons list", a +// data structure frequently found in functional programming languages. Each +// item in a cons list contains two elements: The value of the current item and +// the next item. The last item is a value called `Nil`. + +#[derive(PartialEq, Debug)] +enum List { + Cons(i32, Box), + Nil, +} + +fn create_empty_list() -> List { + List::Nil +} + +fn create_non_empty_list() -> List { + List::Cons(42, Box::new(List::Nil)) +} + +fn main() { + println!("This is an empty cons list: {:?}", create_empty_list()); + println!( + "This is a non-empty cons list: {:?}", + create_non_empty_list(), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_create_empty_list() { + assert_eq!(create_empty_list(), List::Nil); + } + + #[test] + fn test_create_non_empty_list() { + assert_ne!(create_empty_list(), create_non_empty_list()); + } +} From f3842aa746aa77a3fdf0f699951cde0d49f042c4 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 29 Jun 2024 01:20:59 +0200 Subject: [PATCH 404/433] rc1 solution --- exercises/19_smart_pointers/rc1.rs | 21 +++--- rustlings-macros/info.toml | 4 +- solutions/19_smart_pointers/rc1.rs | 105 ++++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 15 deletions(-) diff --git a/exercises/19_smart_pointers/rc1.rs b/exercises/19_smart_pointers/rc1.rs index 19de3db2..ecd34387 100644 --- a/exercises/19_smart_pointers/rc1.rs +++ b/exercises/19_smart_pointers/rc1.rs @@ -1,15 +1,12 @@ // In this exercise, we want to express the concept of multiple owners via the -// Rc type. This is a model of our solar system - there is a Sun type and -// multiple Planets. The Planets take ownership of the sun, indicating that they -// revolve around the sun. -// -// Make this code compile by using the proper Rc primitives to express that the -// sun has multiple owners. +// `Rc` type. This is a model of our solar system - there is a `Sun` type and +// multiple `Planet`s. The planets take ownership of the sun, indicating that +// they revolve around the sun. use std::rc::Rc; #[derive(Debug)] -struct Sun {} +struct Sun; #[derive(Debug)] enum Planet { @@ -25,7 +22,7 @@ enum Planet { impl Planet { fn details(&self) { - println!("Hi from {:?}!", self) + println!("Hi from {self:?}!"); } } @@ -39,7 +36,7 @@ mod tests { #[test] fn rc1() { - let sun = Rc::new(Sun {}); + let sun = Rc::new(Sun); println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference let mercury = Planet::Mercury(Rc::clone(&sun)); @@ -63,17 +60,17 @@ mod tests { jupiter.details(); // TODO - let saturn = Planet::Saturn(Rc::new(Sun {})); + let saturn = Planet::Saturn(Rc::new(Sun)); println!("reference count = {}", Rc::strong_count(&sun)); // 7 references saturn.details(); // TODO - let uranus = Planet::Uranus(Rc::new(Sun {})); + let uranus = Planet::Uranus(Rc::new(Sun)); println!("reference count = {}", Rc::strong_count(&sun)); // 8 references uranus.details(); // TODO - let neptune = Planet::Neptune(Rc::new(Sun {})); + let neptune = Planet::Neptune(Rc::new(Sun)); println!("reference count = {}", Rc::strong_count(&sun)); // 9 references neptune.details(); diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 744ad089..5b3f781c 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -993,11 +993,11 @@ of the `Sun`. After using `drop()` to move the `Planet`s out of scope individually, the reference count goes down. -In the end the `Sun` only has one reference again, to itself. +In the end, the `Sun` only has one reference again, to itself. See more at: https://doc.rust-lang.org/book/ch15-04-rc.html -* Unfortunately Pluto is no longer considered a planet :(""" +Unfortunately, Pluto is no longer considered a planet :(""" [[exercises]] name = "arc1" diff --git a/solutions/19_smart_pointers/rc1.rs b/solutions/19_smart_pointers/rc1.rs index 4e181989..c0a41abf 100644 --- a/solutions/19_smart_pointers/rc1.rs +++ b/solutions/19_smart_pointers/rc1.rs @@ -1 +1,104 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// In this exercise, we want to express the concept of multiple owners via the +// `Rc` type. This is a model of our solar system - there is a `Sun` type and +// multiple `Planet`s. The planets take ownership of the sun, indicating that +// they revolve around the sun. + +use std::rc::Rc; + +#[derive(Debug)] +struct Sun; + +#[derive(Debug)] +enum Planet { + Mercury(Rc), + Venus(Rc), + Earth(Rc), + Mars(Rc), + Jupiter(Rc), + Saturn(Rc), + Uranus(Rc), + Neptune(Rc), +} + +impl Planet { + fn details(&self) { + println!("Hi from {self:?}!"); + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rc1() { + let sun = Rc::new(Sun); + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + let mercury = Planet::Mercury(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + mercury.details(); + + let venus = Planet::Venus(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + venus.details(); + + let earth = Planet::Earth(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + earth.details(); + + let mars = Planet::Mars(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + mars.details(); + + let jupiter = Planet::Jupiter(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + jupiter.details(); + + let saturn = Planet::Saturn(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + saturn.details(); + + // TODO + let uranus = Planet::Uranus(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + uranus.details(); + + // TODO + let neptune = Planet::Neptune(Rc::clone(&sun)); + println!("reference count = {}", Rc::strong_count(&sun)); // 9 references + neptune.details(); + + assert_eq!(Rc::strong_count(&sun), 9); + + drop(neptune); + println!("reference count = {}", Rc::strong_count(&sun)); // 8 references + + drop(uranus); + println!("reference count = {}", Rc::strong_count(&sun)); // 7 references + + drop(saturn); + println!("reference count = {}", Rc::strong_count(&sun)); // 6 references + + drop(jupiter); + println!("reference count = {}", Rc::strong_count(&sun)); // 5 references + + drop(mars); + println!("reference count = {}", Rc::strong_count(&sun)); // 4 references + + drop(earth); + println!("reference count = {}", Rc::strong_count(&sun)); // 3 references + + drop(venus); + println!("reference count = {}", Rc::strong_count(&sun)); // 2 references + + drop(mercury); + println!("reference count = {}", Rc::strong_count(&sun)); // 1 reference + + assert_eq!(Rc::strong_count(&sun), 1); + } +} From a943f5ba32412cf5b8fdd8665c1082ecab3ec545 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 29 Jun 2024 01:48:00 +0200 Subject: [PATCH 405/433] arc1 solution --- exercises/19_smart_pointers/arc1.rs | 55 +++++++++++++++-------------- rustlings-macros/info.toml | 2 +- solutions/19_smart_pointers/arc1.rs | 43 +++++++++++++++++++++- 3 files changed, 72 insertions(+), 28 deletions(-) diff --git a/exercises/19_smart_pointers/arc1.rs b/exercises/19_smart_pointers/arc1.rs index 7b31fa82..c3d714dc 100644 --- a/exercises/19_smart_pointers/arc1.rs +++ b/exercises/19_smart_pointers/arc1.rs @@ -1,39 +1,42 @@ -// In this exercise, we are given a Vec of u32 called "numbers" with values -// ranging from 0 to 99 -- [ 0, 1, 2, ..., 98, 99 ] We would like to use this -// set of numbers within 8 different threads simultaneously. Each thread is -// going to get the sum of every eighth value, with an offset. +// In this exercise, we are given a `Vec` of u32 called `numbers` with values +// ranging from 0 to 99. We would like to use this set of numbers within 8 +// different threads simultaneously. Each thread is going to get the sum of +// every eighth value with an offset. // -// The first thread (offset 0), will sum 0, 8, 16, ... -// The second thread (offset 1), will sum 1, 9, 17, ... -// The third thread (offset 2), will sum 2, 10, 18, ... -// ... -// The eighth thread (offset 7), will sum 7, 15, 23, ... +// The first thread (offset 0), will sum 0, 8, 16, … +// The second thread (offset 1), will sum 1, 9, 17, … +// The third thread (offset 2), will sum 2, 10, 18, … +// … +// The eighth thread (offset 7), will sum 7, 15, 23, … // -// Because we are using threads, our values need to be thread-safe. Therefore, -// we are using Arc. We need to make a change in each of the two TODOs. -// -// Make this code compile by filling in a value for `shared_numbers` where the -// first TODO comment is, and create an initial binding for `child_numbers` -// where the second TODO comment is. Try not to create any copies of the -// `numbers` Vec! +// Because we are using threads, our values need to be thread-safe. Therefore, +// we are using `Arc`. -#![forbid(unused_imports)] // Do not change this, (or the next) line. -use std::sync::Arc; -use std::thread; +// Don't change the lines below. +#![forbid(unused_imports)] +use std::{sync::Arc, thread}; fn main() { let numbers: Vec<_> = (0..100u32).collect(); - let shared_numbers = // TODO - let mut joinhandles = Vec::new(); + + // TODO: Define `shared_numbers` by using `Arc`. + // let shared_numbers = ???; + + let mut join_handles = Vec::new(); for offset in 0..8 { - let child_numbers = // TODO - joinhandles.push(thread::spawn(move || { + // TODO: Define `child_numbers` using `shared_numbers`. + // let child_numbers = ???; + + let handle = thread::spawn(move || { let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum(); - println!("Sum of offset {} is {}", offset, sum); - })); + println!("Sum of offset {offset} is {sum}"); + }); + + join_handles.push(handle); } - for handle in joinhandles.into_iter() { + + for handle in join_handles.into_iter() { handle.join().unwrap(); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 5b3f781c..23b6181e 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1004,7 +1004,7 @@ name = "arc1" dir = "19_smart_pointers" test = false hint = """ -Make `shared_numbers` be an `Arc` from the numbers vector. Then, in order +Make `shared_numbers` be an `Arc` from the `numbers` vector. Then, in order to avoid creating a copy of `numbers`, you'll need to create `child_numbers` inside the loop but still in the main thread. diff --git a/solutions/19_smart_pointers/arc1.rs b/solutions/19_smart_pointers/arc1.rs index 4e181989..a520dfe6 100644 --- a/solutions/19_smart_pointers/arc1.rs +++ b/solutions/19_smart_pointers/arc1.rs @@ -1 +1,42 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// In this exercise, we are given a `Vec` of u32 called `numbers` with values +// ranging from 0 to 99. We would like to use this set of numbers within 8 +// different threads simultaneously. Each thread is going to get the sum of +// every eighth value with an offset. +// +// The first thread (offset 0), will sum 0, 8, 16, … +// The second thread (offset 1), will sum 1, 9, 17, … +// The third thread (offset 2), will sum 2, 10, 18, … +// … +// The eighth thread (offset 7), will sum 7, 15, 23, … +// +// Because we are using threads, our values need to be thread-safe. Therefore, +// we are using `Arc`. + +// Don't change the lines below. +#![forbid(unused_imports)] +use std::{sync::Arc, thread}; + +fn main() { + let numbers: Vec<_> = (0..100u32).collect(); + + let shared_numbers = Arc::new(numbers); + // ^^^^^^^^^^^^^^^^^ + + let mut join_handles = Vec::new(); + + for offset in 0..8 { + let child_numbers = Arc::clone(&shared_numbers); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + let handle = thread::spawn(move || { + let sum: u32 = child_numbers.iter().filter(|&&n| n % 8 == offset).sum(); + println!("Sum of offset {offset} is {sum}"); + }); + + join_handles.push(handle); + } + + for handle in join_handles.into_iter() { + handle.join().unwrap(); + } +} From 663a03a17b2d2001f4f3f35a59cd2e2aa5f2bb24 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 29 Jun 2024 02:07:56 +0200 Subject: [PATCH 406/433] cow1 solution --- exercises/19_smart_pointers/cow1.rs | 78 +++++++++++++---------------- rustlings-macros/info.toml | 6 +-- solutions/19_smart_pointers/cow1.rs | 69 ++++++++++++++++++++++++- 3 files changed, 106 insertions(+), 47 deletions(-) diff --git a/exercises/19_smart_pointers/cow1.rs b/exercises/19_smart_pointers/cow1.rs index 754c0bac..5ecf8482 100644 --- a/exercises/19_smart_pointers/cow1.rs +++ b/exercises/19_smart_pointers/cow1.rs @@ -1,24 +1,18 @@ -// This exercise explores the Cow, or Clone-On-Write type. Cow is a -// clone-on-write smart pointer. It can enclose and provide immutable access to -// borrowed data, and clone the data lazily when mutation or ownership is -// required. The type is designed to work with general borrowed data via the -// Borrow trait. -// -// This exercise is meant to show you what to expect when passing data to Cow. -// Fix the unit tests by checking for Cow::Owned(_) and Cow::Borrowed(_) at the -// TODO markers. +// This exercise explores the `Cow` (Clone-On-Write) smart pointer. It can +// enclose and provide immutable access to borrowed data and clone the data +// lazily when mutation or ownership is required. The type is designed to work +// with general borrowed data via the `Borrow` trait. use std::borrow::Cow; -fn abs_all<'a, 'b>(input: &'a mut Cow<'b, [i32]>) -> &'a mut Cow<'b, [i32]> { - for i in 0..input.len() { - let v = input[i]; - if v < 0 { +fn abs_all(input: &mut Cow<[i32]>) { + for ind in 0..input.len() { + let value = input[ind]; + if value < 0 { // Clones into a vector if not already owned. - input.to_mut()[i] = -v; + input.to_mut()[ind] = -value; } } - input } fn main() { @@ -30,47 +24,45 @@ mod tests { use super::*; #[test] - fn reference_mutation() -> Result<(), &'static str> { + fn reference_mutation() { // Clone occurs because `input` needs to be mutated. - let slice = [-1, 0, 1]; - let mut input = Cow::from(&slice[..]); - match abs_all(&mut input) { - Cow::Owned(_) => Ok(()), - _ => Err("Expected owned value"), - } + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); } #[test] - fn reference_no_mutation() -> Result<(), &'static str> { + fn reference_no_mutation() { // No clone occurs because `input` doesn't need to be mutated. - let slice = [0, 1, 2]; - let mut input = Cow::from(&slice[..]); - match abs_all(&mut input) { - // TODO - } + let vec = vec![0, 1, 2]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + // TODO: Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`. + assert!(matches!(input, todo!())); } #[test] - fn owned_no_mutation() -> Result<(), &'static str> { - // We can also pass `slice` without `&` so Cow owns it directly. In this - // case no mutation occurs and thus also no clone, but the result is + fn owned_no_mutation() { + // We can also pass `vec` without `&` so `Cow` owns it directly. In this + // case, no mutation occurs and thus also no clone. But the result is // still owned because it was never borrowed or mutated. - let slice = vec![0, 1, 2]; - let mut input = Cow::from(slice); - match abs_all(&mut input) { - // TODO - } + let vec = vec![0, 1, 2]; + let mut input = Cow::from(vec); + abs_all(&mut input); + // TODO: Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`. + assert!(matches!(input, todo!())); } #[test] - fn owned_mutation() -> Result<(), &'static str> { + fn owned_mutation() { // Of course this is also the case if a mutation does occur. In this - // case the call to `to_mut()` in the abs_all() function returns a + // case, the call to `to_mut()` in the `abs_all` function returns a // reference to the same data as before. - let slice = vec![-1, 0, 1]; - let mut input = Cow::from(slice); - match abs_all(&mut input) { - // TODO - } + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(vec); + abs_all(&mut input); + // TODO: Replace `todo!()` with `Cow::Owned(_)` or `Cow::Borrowed(_)`. + assert!(matches!(input, todo!())); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 23b6181e..cacdad90 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1020,11 +1020,11 @@ https://doc.rust-lang.org/stable/book/ch16-00-concurrency.html""" name = "cow1" dir = "19_smart_pointers" hint = """ -If `Cow` already owns the data it doesn't need to clone it when `to_mut()` is +If `Cow` already owns the data, it doesn't need to clone it when `to_mut()` is called. -Check out https://doc.rust-lang.org/std/borrow/enum.Cow.html for documentation -on the `Cow` type.""" +Check out the documentation of the `Cow` type: +https://doc.rust-lang.org/std/borrow/enum.Cow.html""" # THREADS diff --git a/solutions/19_smart_pointers/cow1.rs b/solutions/19_smart_pointers/cow1.rs index 4e181989..0a21a91b 100644 --- a/solutions/19_smart_pointers/cow1.rs +++ b/solutions/19_smart_pointers/cow1.rs @@ -1 +1,68 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This exercise explores the `Cow` (Clone-On-Write) smart pointer. It can +// enclose and provide immutable access to borrowed data and clone the data +// lazily when mutation or ownership is required. The type is designed to work +// with general borrowed data via the `Borrow` trait. + +use std::borrow::Cow; + +fn abs_all(input: &mut Cow<[i32]>) { + for ind in 0..input.len() { + let value = input[ind]; + if value < 0 { + // Clones into a vector if not already owned. + input.to_mut()[ind] = -value; + } + } +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reference_mutation() { + // Clone occurs because `input` needs to be mutated. + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); + } + + #[test] + fn reference_no_mutation() { + // No clone occurs because `input` doesn't need to be mutated. + let vec = vec![0, 1, 2]; + let mut input = Cow::from(&vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Borrowed(_))); + // ^^^^^^^^^^^^^^^^ + } + + #[test] + fn owned_no_mutation() { + // We can also pass `vec` without `&` so `Cow` owns it directly. In this + // case, no mutation occurs and thus also no clone. But the result is + // still owned because it was never borrowed or mutated. + let vec = vec![0, 1, 2]; + let mut input = Cow::from(vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); + // ^^^^^^^^^^^^^ + } + + #[test] + fn owned_mutation() { + // Of course this is also the case if a mutation does occur. In this + // case, the call to `to_mut()` in the `abs_all` function returns a + // reference to the same data as before. + let vec = vec![-1, 0, 1]; + let mut input = Cow::from(vec); + abs_all(&mut input); + assert!(matches!(input, Cow::Owned(_))); + // ^^^^^^^^^^^^^ + } +} From b000164eedaf5ada18ce0562aa9b7aed25663458 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 10:59:33 +0200 Subject: [PATCH 407/433] threads1 solution --- exercises/20_threads/threads1.rs | 24 +++++++++++--------- rustlings-macros/info.toml | 2 +- solutions/20_threads/threads1.rs | 38 +++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/exercises/20_threads/threads1.rs b/exercises/20_threads/threads1.rs index bf0b8e0d..01f9ff44 100644 --- a/exercises/20_threads/threads1.rs +++ b/exercises/20_threads/threads1.rs @@ -3,31 +3,35 @@ // wait until all the spawned threads have finished and should collect their // return values into a vector. -use std::thread; -use std::time::{Duration, Instant}; +use std::{ + thread, + time::{Duration, Instant}, +}; fn main() { - let mut handles = vec![]; + let mut handles = Vec::new(); for i in 0..10 { - handles.push(thread::spawn(move || { + let handle = thread::spawn(move || { let start = Instant::now(); thread::sleep(Duration::from_millis(250)); - println!("thread {} is complete", i); + println!("Thread {i} done"); start.elapsed().as_millis() - })); + }); + handles.push(handle); } - let mut results: Vec = vec![]; + let mut results = Vec::new(); for handle in handles { - // TODO: a struct is returned from thread::spawn, can you use it? + // TODO: Collect the results of all threads into the `results` vector. + // Use the `JoinHandle` struct which is returned by `thread::spawn`. } if results.len() != 10 { - panic!("Oh no! All the spawned threads did not finish!"); + panic!("Oh no! Some thread isn't done yet!"); } println!(); for (i, result) in results.into_iter().enumerate() { - println!("thread {} took {}ms", i, result); + println!("Thread {i} took {result}ms"); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index cacdad90..37afa178 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1037,7 +1037,7 @@ hint = """ https://doc.rust-lang.org/std/thread/fn.spawn.html A challenge with multi-threaded applications is that the main thread can -finish before the spawned threads are completed. +finish before the spawned threads are done. https://doc.rust-lang.org/book/ch16-01-threads.html#waiting-for-all-threads-to-finish-using-join-handles Use the `JoinHandle`s to wait for each thread to finish and collect their diff --git a/solutions/20_threads/threads1.rs b/solutions/20_threads/threads1.rs index 4e181989..7f3dd29a 100644 --- a/solutions/20_threads/threads1.rs +++ b/solutions/20_threads/threads1.rs @@ -1 +1,37 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This program spawns multiple threads that each run for at least 250ms, and +// each thread returns how much time they took to complete. The program should +// wait until all the spawned threads have finished and should collect their +// return values into a vector. + +use std::{ + thread, + time::{Duration, Instant}, +}; + +fn main() { + let mut handles = Vec::new(); + for i in 0..10 { + let handle = thread::spawn(move || { + let start = Instant::now(); + thread::sleep(Duration::from_millis(250)); + println!("Thread {i} done"); + start.elapsed().as_millis() + }); + handles.push(handle); + } + + let mut results = Vec::new(); + for handle in handles { + // Collect the results of all threads into the `results` vector. + results.push(handle.join().unwrap()); + } + + if results.len() != 10 { + panic!("Oh no! Some thread isn't done yet!"); + } + + println!(); + for (i, result) in results.into_iter().enumerate() { + println!("Thread {i} took {result}ms"); + } +} From dfa2b44f718906dfac54816bb582880066c3dff0 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:11:11 +0200 Subject: [PATCH 408/433] threads2 solution --- exercises/20_threads/threads2.rs | 27 ++++++++++---------- rustlings-macros/info.toml | 10 ++++---- solutions/20_threads/threads2.rs | 42 +++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 20 deletions(-) diff --git a/exercises/20_threads/threads2.rs b/exercises/20_threads/threads2.rs index 2bdeba94..7020cb9c 100644 --- a/exercises/20_threads/threads2.rs +++ b/exercises/20_threads/threads2.rs @@ -1,35 +1,34 @@ // Building on the last exercise, we want all of the threads to complete their -// work but this time the spawned threads need to be in charge of updating a -// shared value: JobStatus.jobs_completed +// work. But this time, the spawned threads need to be in charge of updating a +// shared value: `JobStatus.jobs_done` -use std::sync::Arc; -use std::thread; -use std::time::Duration; +use std::{sync::Arc, thread, time::Duration}; struct JobStatus { - jobs_completed: u32, + jobs_done: u32, } fn main() { - // TODO: `Arc` isn't enough if you want a **mutable** shared state - let status = Arc::new(JobStatus { jobs_completed: 0 }); + // TODO: `Arc` isn't enough if you want a **mutable** shared state. + let status = Arc::new(JobStatus { jobs_done: 0 }); - let mut handles = vec![]; + let mut handles = Vec::new(); for _ in 0..10 { let status_shared = Arc::clone(&status); let handle = thread::spawn(move || { thread::sleep(Duration::from_millis(250)); - // TODO: You must take an action before you update a shared value - status_shared.jobs_completed += 1; + + // TODO: You must take an action before you update a shared value. + status_shared.jobs_done += 1; }); handles.push(handle); } - // Waiting for all jobs to complete + // Waiting for all jobs to complete. for handle in handles { handle.join().unwrap(); } - // TODO: Print the value of `JobStatus.jobs_completed` - println!("Jobs completed: {}", ???); + // TODO: Print the value of `JobStatus.jobs_done`. + println!("Jobs done: {}", todo!()); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 37afa178..ab8c1215 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1051,19 +1051,19 @@ dir = "20_threads" test = false hint = """ `Arc` is an Atomic Reference Counted pointer that allows safe, shared access -to **immutable** data. But we want to *change* the number of `jobs_completed` -so we'll need to also use another type that will only allow one thread to -mutate the data at a time. Take a look at this section of the book: +to **immutable** data. But we want to *change* the number of `jobs_done` so +we'll need to also use another type that will only allow one thread to mutate +the data at a time. Take a look at this section of the book: https://doc.rust-lang.org/book/ch16-03-shared-state.html#atomic-reference-counting-with-arct Keep reading if you'd like more hints :) Do you now have an `Arc>` at the beginning of `main`? Like: ``` -let status = Arc::new(Mutex::new(JobStatus { jobs_completed: 0 })); +let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 })); ``` -Similar to the code in the following example in the book: +Similar to the code in the following example in The Book: https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads""" [[exercises]] diff --git a/solutions/20_threads/threads2.rs b/solutions/20_threads/threads2.rs index 4e181989..bc268d63 100644 --- a/solutions/20_threads/threads2.rs +++ b/solutions/20_threads/threads2.rs @@ -1 +1,41 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Building on the last exercise, we want all of the threads to complete their +// work. But this time, the spawned threads need to be in charge of updating a +// shared value: `JobStatus.jobs_done` + +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +struct JobStatus { + jobs_done: u32, +} + +fn main() { + // `Arc` isn't enough if you want a **mutable** shared state. + // We need to wrap the value with a `Mutex`. + let status = Arc::new(Mutex::new(JobStatus { jobs_done: 0 })); + // ^^^^^^^^^^^ ^ + + let mut handles = Vec::new(); + for _ in 0..10 { + let status_shared = Arc::clone(&status); + let handle = thread::spawn(move || { + thread::sleep(Duration::from_millis(250)); + + // Lock before you update a shared value. + status_shared.lock().unwrap().jobs_done += 1; + // ^^^^^^^^^^^^^^^^ + }); + handles.push(handle); + } + + // Waiting for all jobs to complete. + for handle in handles { + handle.join().unwrap(); + } + + println!("Jobs done: {}", status.lock().unwrap().jobs_done); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +} From a13e3cd07f86e8668a326bae98568cced61f5015 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:23:40 +0200 Subject: [PATCH 409/433] threads3 solution --- exercises/20_threads/threads3.rs | 23 ++++++----- rustlings-macros/info.toml | 5 ++- solutions/20_threads/threads3.rs | 67 +++++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 15 deletions(-) diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index 37810cf9..30ac8ddd 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -1,7 +1,4 @@ -use std::sync::mpsc; -use std::sync::Arc; -use std::thread; -use std::time::Duration; +use std::{sync::mpsc, thread, time::Duration}; struct Queue { length: u32, @@ -11,7 +8,7 @@ struct Queue { impl Queue { fn new() -> Self { - Queue { + Self { length: 10, first_half: vec![1, 2, 3, 4, 5], second_half: vec![6, 7, 8, 9, 10], @@ -19,20 +16,22 @@ impl Queue { } } -fn send_tx(q: Queue, tx: mpsc::Sender) -> () { +fn send_tx(q: Queue, tx: mpsc::Sender) { + // TODO: We want to send `tx` to both threads. But currently, it is moved + // into the frist thread. How could you solve this problem? thread::spawn(move || { for val in q.first_half { - println!("sending {:?}", val); + println!("Sending {val:?}"); tx.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); + thread::sleep(Duration::from_millis(250)); } }); thread::spawn(move || { for val in q.second_half { - println!("sending {:?}", val); + println!("Sending {val:?}"); tx.send(val).unwrap(); - thread::sleep(Duration::from_secs(1)); + thread::sleep(Duration::from_millis(250)); } }); } @@ -55,11 +54,11 @@ mod tests { let mut total_received: u32 = 0; for received in rx { - println!("Got: {}", received); + println!("Got: {received}"); total_received += 1; } - println!("total numbers received: {}", total_received); + println!("Number of received values: {total_received}"); assert_eq!(total_received, queue_length); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index ab8c1215..24dcdee2 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1076,10 +1076,11 @@ An alternate way to handle concurrency between threads is to use an `mpsc` With both a sending end and a receiving end, it's possible to send values in one thread and receive them in another. -Multiple producers are possible by using clone() to create a duplicate of the +Multiple producers are possible by using `clone()` to create a duplicate of the original sending end. -See https://doc.rust-lang.org/book/ch16-02-message-passing.html for more info.""" +Related section in The Book: +https://doc.rust-lang.org/book/ch16-02-message-passing.html""" # MACROS diff --git a/solutions/20_threads/threads3.rs b/solutions/20_threads/threads3.rs index 4e181989..cd2dfbe3 100644 --- a/solutions/20_threads/threads3.rs +++ b/solutions/20_threads/threads3.rs @@ -1 +1,66 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +use std::{sync::mpsc, thread, time::Duration}; + +struct Queue { + length: u32, + first_half: Vec, + second_half: Vec, +} + +impl Queue { + fn new() -> Self { + Self { + length: 10, + first_half: vec![1, 2, 3, 4, 5], + second_half: vec![6, 7, 8, 9, 10], + } + } +} + +fn send_tx(q: Queue, tx: mpsc::Sender) { + // Clone the sender `tx` first. + let tx_clone = tx.clone(); + thread::spawn(move || { + for val in q.first_half { + println!("Sending {val:?}"); + // Then use the clone in the first thread. This means that + // `tx_clone` is moved to the first thread and `tx` to the second. + tx_clone.send(val).unwrap(); + thread::sleep(Duration::from_millis(250)); + } + }); + + thread::spawn(move || { + for val in q.second_half { + println!("Sending {val:?}"); + tx.send(val).unwrap(); + thread::sleep(Duration::from_millis(250)); + } + }); +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn threads3() { + let (tx, rx) = mpsc::channel(); + let queue = Queue::new(); + let queue_length = queue.length; + + send_tx(queue, tx); + + let mut total_received: u32 = 0; + for received in rx { + println!("Got: {received}"); + total_received += 1; + } + + println!("Number of received values: {total_received}"); + assert_eq!(total_received, queue_length); + } +} From cf90364fd779255074eac9a7d90c53ad657936ba Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:28:38 +0200 Subject: [PATCH 410/433] macros1 solution --- exercises/21_macros/macros1.rs | 1 + rustlings-macros/info.toml | 5 ++--- solutions/21_macros/macros1.rs | 11 ++++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/exercises/21_macros/macros1.rs b/exercises/21_macros/macros1.rs index 1d415cb1..fb3c3ff9 100644 --- a/exercises/21_macros/macros1.rs +++ b/exercises/21_macros/macros1.rs @@ -5,5 +5,6 @@ macro_rules! my_macro { } fn main() { + // TODO: Fix the macro call. my_macro(); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 24dcdee2..7dcf3441 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1089,9 +1089,8 @@ name = "macros1" dir = "21_macros" test = false hint = """ -When you call a macro, you need to add something special compared to a -regular function call. If you're stuck, take a look at what's inside -`my_macro`.""" +When you call a macro, you need to add something special compared to a regular +function call.""" [[exercises]] name = "macros2" diff --git a/solutions/21_macros/macros1.rs b/solutions/21_macros/macros1.rs index 4e181989..1b861564 100644 --- a/solutions/21_macros/macros1.rs +++ b/solutions/21_macros/macros1.rs @@ -1 +1,10 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; +} + +fn main() { + my_macro!(); + // ^ +} From 9845e046de6f9569519d0e0ae3c50341eb35a8bf Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:31:37 +0200 Subject: [PATCH 411/433] macros2 solution --- exercises/21_macros/macros2.rs | 1 + solutions/21_macros/macros2.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/exercises/21_macros/macros2.rs b/exercises/21_macros/macros2.rs index f16712be..2d9dec76 100644 --- a/exercises/21_macros/macros2.rs +++ b/exercises/21_macros/macros2.rs @@ -2,6 +2,7 @@ fn main() { my_macro!(); } +// TODO: Fix the compiler error by moving the whole definition of this macro. macro_rules! my_macro { () => { println!("Check out my macro!"); diff --git a/solutions/21_macros/macros2.rs b/solutions/21_macros/macros2.rs index 4e181989..b6fd5d2c 100644 --- a/solutions/21_macros/macros2.rs +++ b/solutions/21_macros/macros2.rs @@ -1 +1,10 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Moved the macro definition to be before its call. +macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; +} + +fn main() { + my_macro!(); +} From 4cb15a4cda4791134a75a0462031b5e86b45fa0d Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:37:48 +0200 Subject: [PATCH 412/433] macros3 solution --- exercises/21_macros/macros3.rs | 4 ++-- rustlings-macros/info.toml | 5 +---- solutions/21_macros/macros3.rs | 14 +++++++++++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/exercises/21_macros/macros3.rs b/exercises/21_macros/macros3.rs index 405c397a..95374948 100644 --- a/exercises/21_macros/macros3.rs +++ b/exercises/21_macros/macros3.rs @@ -1,5 +1,5 @@ -// Make me compile, without taking the macro out of the module! - +// TODO: Fix the compiler error without taking the macro definition out of this +// module. mod macros { macro_rules! my_macro { () => { diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 7dcf3441..0ec5fb21 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1109,10 +1109,7 @@ dir = "21_macros" test = false hint = """ In order to use a macro outside of its module, you need to do something -special to the module to lift the macro out into its parent. - -The same trick also works on "extern crate" statements for crates that have -exported macros, if you've seen any of those around.""" +special to the module to lift the macro out into its parent.""" [[exercises]] name = "macros4" diff --git a/solutions/21_macros/macros3.rs b/solutions/21_macros/macros3.rs index 4e181989..df35be4d 100644 --- a/solutions/21_macros/macros3.rs +++ b/solutions/21_macros/macros3.rs @@ -1 +1,13 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Added the attribute `macro_use` attribute. +#[macro_use] +mod macros { + macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; + } +} + +fn main() { + my_macro!(); +} From cc2c0958c9ba038e1584f3cbff0b07df4cc490c1 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:54:05 +0200 Subject: [PATCH 413/433] macros4 solution --- exercises/21_macros/macros4.rs | 1 + solutions/21_macros/macros4.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/exercises/21_macros/macros4.rs b/exercises/21_macros/macros4.rs index 03ece080..9d77f6a5 100644 --- a/exercises/21_macros/macros4.rs +++ b/exercises/21_macros/macros4.rs @@ -1,3 +1,4 @@ +// TODO: Fix the compiler error by adding one or two characters. #[rustfmt::skip] macro_rules! my_macro { () => { diff --git a/solutions/21_macros/macros4.rs b/solutions/21_macros/macros4.rs index 4e181989..41bcad16 100644 --- a/solutions/21_macros/macros4.rs +++ b/solutions/21_macros/macros4.rs @@ -1 +1,15 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Added semicolons to separate the macro arms. +#[rustfmt::skip] +macro_rules! my_macro { + () => { + println!("Check out my macro!"); + }; + ($val:expr) => { + println!("Look at this other macro: {}", $val); + }; +} + +fn main() { + my_macro!(); + my_macro!(7777); +} From 78728d52387730300475cbe8c83497f603a14faf Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:54:35 +0200 Subject: [PATCH 414/433] clippy1 solution --- exercises/22_clippy/clippy1.rs | 16 +++++++--------- rustlings-macros/info.toml | 4 ++-- solutions/22_clippy/clippy1.rs | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/exercises/22_clippy/clippy1.rs b/exercises/22_clippy/clippy1.rs index f1eaa831..b9d1ec17 100644 --- a/exercises/22_clippy/clippy1.rs +++ b/exercises/22_clippy/clippy1.rs @@ -1,19 +1,17 @@ // The Clippy tool is a collection of lints to analyze your code so you can // catch common mistakes and improve your Rust code. // -// For these exercises the code will fail to compile when there are Clippy +// For these exercises, the code will fail to compile when there are Clippy // warnings. Check Clippy's suggestions from the output to solve the exercise. -use std::f32; +use std::f32::consts::PI; fn main() { - let pi = 3.14f32; - let radius = 5.00f32; + // Use the more accurate `PI` constant. + let pi = PI; + let radius: f32 = 5.0; - let area = pi * f32::powi(radius, 2); + let area = pi * radius.powi(2); - println!( - "The area of a circle with radius {:.2} is {:.5}!", - radius, area - ) + println!("The area of a circle with radius {radius:.2} is {area:.5}"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 0ec5fb21..4d40726e 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1134,7 +1134,7 @@ dir = "22_clippy" test = false strict_clippy = true hint = """ -Rust stores the highest precision version of any long or infinite precision +Rust stores the highest precision version of some long or infinite precision mathematical constants in the Rust standard library: https://doc.rust-lang.org/stable/std/f32/consts/index.html @@ -1142,7 +1142,7 @@ We may be tempted to use our own approximations for certain mathematical constants, but clippy recognizes those imprecise mathematical constants as a source of potential error. -See the suggestions of the clippy warning in compile output and use the +See the suggestions of the Clippy warning in the compile output and use the appropriate replacement constant from `std::f32::consts`...""" [[exercises]] diff --git a/solutions/22_clippy/clippy1.rs b/solutions/22_clippy/clippy1.rs index 4e181989..b9d1ec17 100644 --- a/solutions/22_clippy/clippy1.rs +++ b/solutions/22_clippy/clippy1.rs @@ -1 +1,17 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The Clippy tool is a collection of lints to analyze your code so you can +// catch common mistakes and improve your Rust code. +// +// For these exercises, the code will fail to compile when there are Clippy +// warnings. Check Clippy's suggestions from the output to solve the exercise. + +use std::f32::consts::PI; + +fn main() { + // Use the more accurate `PI` constant. + let pi = PI; + let radius: f32 = 5.0; + + let area = pi * radius.powi(2); + + println!("The area of a circle with radius {radius:.2} is {area:.5}"); +} From a0e810b4713bcef60f64f4709ee27c3acec676cd Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 11:55:18 +0200 Subject: [PATCH 415/433] clippy2 solution --- exercises/22_clippy/clippy2.rs | 4 +++- rustlings-macros/info.toml | 3 ++- solutions/22_clippy/clippy2.rs | 11 ++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/exercises/22_clippy/clippy2.rs b/exercises/22_clippy/clippy2.rs index c7d400d1..8cfe6f80 100644 --- a/exercises/22_clippy/clippy2.rs +++ b/exercises/22_clippy/clippy2.rs @@ -1,8 +1,10 @@ fn main() { let mut res = 42; let option = Some(12); + // TODO: Fix the Clippy lint. for x in option { res += x; } - println!("{}", res); + + println!("{res}"); } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 4d40726e..fce5e5a3 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1151,7 +1151,8 @@ dir = "22_clippy" test = false strict_clippy = true hint = """ -`for` loops over `Option` values are more clearly expressed as an `if let`""" +`for` loops over `Option` values are more clearly expressed as an `if-let` +statement.""" [[exercises]] name = "clippy3" diff --git a/solutions/22_clippy/clippy2.rs b/solutions/22_clippy/clippy2.rs index 4e181989..7f635628 100644 --- a/solutions/22_clippy/clippy2.rs +++ b/solutions/22_clippy/clippy2.rs @@ -1 +1,10 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +fn main() { + let mut res = 42; + let option = Some(12); + // Use `if-let` instead of iteration. + if let Some(x) = option { + res += x; + } + + println!("{res}"); +} From 09c94bef2dbaf44daf81d8f618289c9425d1f90f Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 12:09:52 +0200 Subject: [PATCH 416/433] clippy3 solution --- exercises/22_clippy/clippy3.rs | 12 +++++++----- solutions/22_clippy/clippy3.rs | 32 +++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/exercises/22_clippy/clippy3.rs b/exercises/22_clippy/clippy3.rs index fd829cf6..4f788349 100644 --- a/exercises/22_clippy/clippy3.rs +++ b/exercises/22_clippy/clippy3.rs @@ -1,25 +1,27 @@ -// Here's a couple more easy Clippy fixes, so you can see its utility. +// Here are some more easy Clippy fixes so you can see its utility 📎 +// TODO: Fix all the Clippy lints. +#[rustfmt::skip] #[allow(unused_variables, unused_assignments)] fn main() { let my_option: Option<()> = None; if my_option.is_none() { - my_option.unwrap(); + println!("{:?}", my_option.unwrap()); } let my_arr = &[ -1, -2, -3 -4, -5, -6 ]; - println!("My array! Here it is: {:?}", my_arr); + println!("My array! Here it is: {my_arr:?}"); let my_empty_vec = vec![1, 2, 3, 4, 5].resize(0, 5); - println!("This Vec is empty, see? {:?}", my_empty_vec); + println!("This Vec is empty, see? {my_empty_vec:?}"); let mut value_a = 45; let mut value_b = 66; // Let's swap these two! value_a = value_b; value_b = value_a; - println!("value a: {}; value b: {}", value_a, value_b); + println!("value a: {value_a}; value b: {value_b}"); } diff --git a/solutions/22_clippy/clippy3.rs b/solutions/22_clippy/clippy3.rs index 4e181989..811d1847 100644 --- a/solutions/22_clippy/clippy3.rs +++ b/solutions/22_clippy/clippy3.rs @@ -1 +1,31 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +use std::mem; + +#[rustfmt::skip] +#[allow(unused_variables, unused_assignments)] +fn main() { + let my_option: Option<()> = None; + // `unwrap` of an `Option` after checking if it is `None` will panic. + // Use `if-let` instead. + if let Some(value) = my_option { + println!("{value:?}"); + } + + // A comma was missing. + let my_arr = &[ + -1, -2, -3, + -4, -5, -6, + ]; + println!("My array! Here it is: {:?}", my_arr); + + let mut my_empty_vec = vec![1, 2, 3, 4, 5]; + // `resize` mutates a vector instead of returning a new one. + // `resize(0, …)` clears a vector, so it is better to use `clear`. + my_empty_vec.clear(); + println!("This Vec is empty, see? {my_empty_vec:?}"); + + let mut value_a = 45; + let mut value_b = 66; + // Use `mem::swap` to correctly swap two values. + mem::swap(&mut value_a, &mut value_b); + println!("value a: {}; value b: {}", value_a, value_b); +} From 428d64ffa01415826e421e00f59f63a77884b923 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 21:41:22 +0200 Subject: [PATCH 417/433] using_as solution --- exercises/23_conversions/using_as.rs | 10 ++++------ solutions/23_conversions/using_as.rs | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/exercises/23_conversions/using_as.rs b/exercises/23_conversions/using_as.rs index 94b1bb31..c131d1f3 100644 --- a/exercises/23_conversions/using_as.rs +++ b/exercises/23_conversions/using_as.rs @@ -1,12 +1,10 @@ -// Type casting in Rust is done via the usage of the `as` operator. Please note -// that the `as` operator is not only used when type casting. It also helps with -// renaming imports. -// -// The goal is to make sure that the division does not fail to compile and -// returns the proper type. +// Type casting in Rust is done via the usage of the `as` operator. +// Note that the `as` operator is not only used when type casting. It also helps +// with renaming imports. fn average(values: &[f64]) -> f64 { let total = values.iter().sum::(); + // TODO: Make a conversion before dividing. total / values.len() } diff --git a/solutions/23_conversions/using_as.rs b/solutions/23_conversions/using_as.rs index 4e181989..14b92ebf 100644 --- a/solutions/23_conversions/using_as.rs +++ b/solutions/23_conversions/using_as.rs @@ -1 +1,24 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// Type casting in Rust is done via the usage of the `as` operator. +// Note that the `as` operator is not only used when type casting. It also helps +// with renaming imports. + +fn average(values: &[f64]) -> f64 { + let total = values.iter().sum::(); + total / values.len() as f64 + // ^^^^^^ +} + +fn main() { + let values = [3.5, 0.3, 13.0, 11.7]; + println!("{}", average(&values)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn returns_proper_type_and_value() { + assert_eq!(average(&[3.5, 0.3, 13.0, 11.7]), 7.125); + } +} From cddaf4881ea5a03e6deebfa9ec949347e1c2d025 Mon Sep 17 00:00:00 2001 From: mo8it Date: Mon, 1 Jul 2024 22:09:48 +0200 Subject: [PATCH 418/433] from_into solution --- exercises/23_conversions/from_into.rs | 62 ++++++------ rustlings-macros/info.toml | 2 +- solutions/23_conversions/from_into.rs | 137 +++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 34 deletions(-) diff --git a/exercises/23_conversions/from_into.rs b/exercises/23_conversions/from_into.rs index 14a62ba8..bc2783a3 100644 --- a/exercises/23_conversions/from_into.rs +++ b/exercises/23_conversions/from_into.rs @@ -1,81 +1,79 @@ -// The From trait is used for value-to-value conversions. If From is implemented -// correctly for a type, the Into trait should work conversely. You can read -// more about it at https://doc.rust-lang.org/std/convert/trait.From.html +// The `From` trait is used for value-to-value conversions. If `From` is +// implemented, an implementation of `Into` is automatically provided. +// You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.From.html #[derive(Debug)] struct Person { name: String, - age: usize, + age: u8, } -// We implement the Default trait to use it as a fallback -// when the provided string is not convertible into a Person object +// We implement the Default trait to use it as a fallback when the provided +// string is not convertible into a `Person` object. impl Default for Person { - fn default() -> Person { - Person { + fn default() -> Self { + Self { name: String::from("John"), age: 30, } } } -// Your task is to complete this implementation in order for the line `let p1 = -// Person::from("Mark,20")` to compile. Please note that you'll need to parse the -// age component into a `usize` with something like `"4".parse::()`. The -// outcome of this needs to be handled appropriately. +// TODO: Complete this `From` implementation to be able to parse a `Person` +// out of a string in the form of "Mark,20". +// Note that you'll need to parse the age component into a `u8` with something +// like `"4".parse::()`. // // Steps: -// 1. If the length of the provided string is 0, then return the default of -// Person. -// 2. Split the given string on the commas present in it. -// 3. Extract the first element from the split operation and use it as the name. -// 4. If the name is empty, then return the default of Person. -// 5. Extract the other element from the split operation and parse it into a -// `usize` as the age. -// If while parsing the age, something goes wrong, then return the default of -// Person. Otherwise, then return an instantiated Person object with the results - +// 1. Split the given string on the commas present in it. +// 2. If the split operation returns less or more than 2 elements, return the +// default of `Person`. +// 3. Use the first element from the split operation as the name. +// 4. If the name is empty, return the default of `Person`. +// 5. Parse the second element from the split operation into a `u8` as the age. +// 6. If parsing the age fails, return the default of `Person`. impl From<&str> for Person { - fn from(s: &str) -> Person {} + fn from(s: &str) -> Self {} } fn main() { - // Use the `from` function + // Use the `from` function. let p1 = Person::from("Mark,20"); - // Since From is implemented for Person, we should be able to use Into + println!("{p1:?}"); + + // Since `From` is implemented for Person, we are able to use `Into`. let p2: Person = "Gerald,70".into(); - println!("{:?}", p1); - println!("{:?}", p2); + println!("{p2:?}"); } #[cfg(test)] mod tests { use super::*; + #[test] fn test_default() { - // Test that the default person is 30 year old John let dp = Person::default(); assert_eq!(dp.name, "John"); assert_eq!(dp.age, 30); } + #[test] fn test_bad_convert() { - // Test that John is returned when bad string is provided let p = Person::from(""); assert_eq!(p.name, "John"); assert_eq!(p.age, 30); } + #[test] fn test_good_convert() { - // Test that "Mark,20" works let p = Person::from("Mark,20"); assert_eq!(p.name, "Mark"); assert_eq!(p.age, 20); } + #[test] fn test_bad_age() { - // Test that "Mark,twenty" will return the default person due to an - // error in parsing age let p = Person::from("Mark,twenty"); assert_eq!(p.name, "John"); assert_eq!(p.age, 30); diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index fce5e5a3..b848e0ef 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1174,7 +1174,7 @@ Use the `as` operator to cast one of the operands in the last line of the name = "from_into" dir = "23_conversions" hint = """ -Follow the steps provided right before the `From` implementation""" +Follow the steps provided right before the `From` implementation.""" [[exercises]] name = "from_str" diff --git a/solutions/23_conversions/from_into.rs b/solutions/23_conversions/from_into.rs index 4e181989..cec23cb4 100644 --- a/solutions/23_conversions/from_into.rs +++ b/solutions/23_conversions/from_into.rs @@ -1 +1,136 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// The `From` trait is used for value-to-value conversions. If `From` is +// implemented, an implementation of `Into` is automatically provided. +// You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.From.html + +#[derive(Debug)] +struct Person { + name: String, + age: u8, +} + +// We implement the Default trait to use it as a fallback when the provided +// string is not convertible into a `Person` object. +impl Default for Person { + fn default() -> Self { + Self { + name: String::from("John"), + age: 30, + } + } +} + +impl From<&str> for Person { + fn from(s: &str) -> Self { + let mut split = s.split(','); + let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { + // ^^^^ there should be no third element + return Self::default(); + }; + + if name.is_empty() { + return Self::default(); + } + + let Ok(age) = age.parse() else { + return Self::default(); + }; + + Self { + name: name.into(), + age, + } + } +} + +fn main() { + // Use the `from` function. + let p1 = Person::from("Mark,20"); + println!("{p1:?}"); + + // Since `From` is implemented for Person, we are able to use `Into`. + let p2: Person = "Gerald,70".into(); + println!("{p2:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default() { + let dp = Person::default(); + assert_eq!(dp.name, "John"); + assert_eq!(dp.age, 30); + } + + #[test] + fn test_bad_convert() { + let p = Person::from(""); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_good_convert() { + let p = Person::from("Mark,20"); + assert_eq!(p.name, "Mark"); + assert_eq!(p.age, 20); + } + + #[test] + fn test_bad_age() { + let p = Person::from("Mark,twenty"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_comma_and_age() { + let p: Person = Person::from("Mark"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_age() { + let p: Person = Person::from("Mark,"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_name() { + let p: Person = Person::from(",1"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_name_and_age() { + let p: Person = Person::from(","); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_missing_name_and_invalid_age() { + let p: Person = Person::from(",one"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_trailing_comma() { + let p: Person = Person::from("Mike,32,"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } + + #[test] + fn test_trailing_comma_and_some_string() { + let p: Person = Person::from("Mike,32,dog"); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 30); + } +} From e3c8c457ba8744b0f1b799c4d7d4bf24e8e61792 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:03:55 +0200 Subject: [PATCH 419/433] from_str solution --- exercises/23_conversions/from_str.rs | 60 +++++++------ rustlings-macros/info.toml | 10 +-- solutions/23_conversions/from_str.rs | 129 ++++++++++++++++++++++++++- 3 files changed, 163 insertions(+), 36 deletions(-) diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index 58270f02..1b3f553e 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -1,7 +1,8 @@ -// This is similar to from_into.rs, but this time we'll implement `FromStr` and -// return errors instead of falling back to a default value. Additionally, upon -// implementing FromStr, you can use the `parse` method on strings to generate -// an object of the implementor type. You can read more about it at +// This is similar to the previous `from_into` exercise. But this time, we'll +// implement `FromStr` and return errors instead of falling back to a default +// value. Additionally, upon implementing `FromStr`, you can use the `parse` +// method on strings to generate an object of the implementor type. You can read +// more about it in the documentation: // https://doc.rust-lang.org/std/str/trait.FromStr.html use std::num::ParseIntError; @@ -10,43 +11,42 @@ use std::str::FromStr; #[derive(Debug, PartialEq)] struct Person { name: String, - age: usize, + age: u8, } // We will use this error type for the `FromStr` implementation. #[derive(Debug, PartialEq)] enum ParsePersonError { - // Empty input string - Empty, // Incorrect number of fields BadLen, // Empty name field NoName, - // Wrapped error from parse::() + // Wrapped error from parse::() ParseInt(ParseIntError), } +// TODO: Complete this `From` implementation to be able to parse a `Person` +// out of a string in the form of "Mark,20". +// Note that you'll need to parse the age component into a `u8` with something +// like `"4".parse::()`. +// // Steps: -// 1. If the length of the provided string is 0, an error should be returned -// 2. Split the given string on the commas present in it -// 3. Only 2 elements should be returned from the split, otherwise return an -// error -// 4. Extract the first element from the split operation and use it as the name -// 5. Extract the other element from the split operation and parse it into a -// `usize` as the age with something like `"4".parse::()` -// 6. If while extracting the name and the age something goes wrong, an error -// should be returned -// If everything goes well, then return a Result of a Person object - +// 1. Split the given string on the commas present in it. +// 2. If the split operation returns less or more than 2 elements, return the +// error `ParsePersonError::BadLen`. +// 3. Use the first element from the split operation as the name. +// 4. If the name is empty, return the error `ParsePersonError::NoName`. +// 5. Parse the second element from the split operation into a `u8` as the age. +// 6. If parsing the age fails, return the error `ParsePersonError::ParseInt`. impl FromStr for Person { type Err = ParsePersonError; - fn from_str(s: &str) -> Result { - } + + fn from_str(s: &str) -> Result {} } fn main() { - let p = "Mark,20".parse::().unwrap(); - println!("{:?}", p); + let p = "Mark,20".parse::(); + println!("{p:?}"); } #[cfg(test)] @@ -55,8 +55,9 @@ mod tests { #[test] fn empty_input() { - assert_eq!("".parse::(), Err(ParsePersonError::Empty)); + assert_eq!("".parse::(), Err(ParsePersonError::BadLen)); } + #[test] fn good_input() { let p = "John,32".parse::(); @@ -65,11 +66,12 @@ mod tests { assert_eq!(p.name, "John"); assert_eq!(p.age, 32); } + #[test] fn missing_age() { assert!(matches!( "John,".parse::(), - Err(ParsePersonError::ParseInt(_)) + Err(ParsePersonError::ParseInt(_)), )); } @@ -77,7 +79,7 @@ mod tests { fn invalid_age() { assert!(matches!( "John,twenty".parse::(), - Err(ParsePersonError::ParseInt(_)) + Err(ParsePersonError::ParseInt(_)), )); } @@ -95,7 +97,7 @@ mod tests { fn missing_name_and_age() { assert!(matches!( ",".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)) + Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), )); } @@ -103,7 +105,7 @@ mod tests { fn missing_name_and_invalid_age() { assert!(matches!( ",one".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)) + Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), )); } @@ -116,7 +118,7 @@ mod tests { fn trailing_comma_and_some_string() { assert_eq!( "John,32,man".parse::(), - Err(ParsePersonError::BadLen) + Err(ParsePersonError::BadLen), ); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index b848e0ef..4ef1a0a3 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1183,13 +1183,11 @@ hint = """ The implementation of `FromStr` should return an `Ok` with a `Person` object, or an `Err` with an error if the string is not valid. -This is almost like the `from_into` exercise, but returning errors instead -of falling back to a default value. +This is almost like the previous `from_into` exercise, but returning errors +instead of falling back to a default value. -Look at the test cases to see which error variants to return. - -Another hint: You can use the `map_err` method of `Result` with a function -or a closure to wrap the error from `parse::`. +Another hint: You can use the `map_err` method of `Result` with a function or a +closure to wrap the error from `parse::`. Yet another hint: If you would like to propagate errors by using the `?` operator in your solution, you might want to look at diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/from_str.rs index 4e181989..301150b9 100644 --- a/solutions/23_conversions/from_str.rs +++ b/solutions/23_conversions/from_str.rs @@ -1 +1,128 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// This is similar to the previous `from_into` exercise. But this time, we'll +// implement `FromStr` and return errors instead of falling back to a default +// value. Additionally, upon implementing `FromStr`, you can use the `parse` +// method on strings to generate an object of the implementor type. You can read +// more about it in the documentation: +// https://doc.rust-lang.org/std/str/trait.FromStr.html + +use std::num::ParseIntError; +use std::str::FromStr; + +#[derive(Debug, PartialEq)] +struct Person { + name: String, + age: u8, +} + +// We will use this error type for the `FromStr` implementation. +#[derive(Debug, PartialEq)] +enum ParsePersonError { + // Incorrect number of fields + BadLen, + // Empty name field + NoName, + // Wrapped error from parse::() + ParseInt(ParseIntError), +} + +impl FromStr for Person { + type Err = ParsePersonError; + + fn from_str(s: &str) -> Result { + let mut split = s.split(','); + let (Some(name), Some(age), None) = (split.next(), split.next(), split.next()) else { + // ^^^^ there should be no third element + return Err(ParsePersonError::BadLen); + }; + + if name.is_empty() { + return Err(ParsePersonError::NoName); + } + + let age = age.parse().map_err(ParsePersonError::ParseInt)?; + + Ok(Self { + name: name.into(), + age, + }) + } +} + +fn main() { + let p = "Mark,20".parse::(); + println!("{p:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_input() { + assert_eq!("".parse::(), Err(ParsePersonError::BadLen)); + } + + #[test] + fn good_input() { + let p = "John,32".parse::(); + assert!(p.is_ok()); + let p = p.unwrap(); + assert_eq!(p.name, "John"); + assert_eq!(p.age, 32); + } + + #[test] + fn missing_age() { + assert!(matches!( + "John,".parse::(), + Err(ParsePersonError::ParseInt(_)), + )); + } + + #[test] + fn invalid_age() { + assert!(matches!( + "John,twenty".parse::(), + Err(ParsePersonError::ParseInt(_)), + )); + } + + #[test] + fn missing_comma_and_age() { + assert_eq!("John".parse::(), Err(ParsePersonError::BadLen)); + } + + #[test] + fn missing_name() { + assert_eq!(",1".parse::(), Err(ParsePersonError::NoName)); + } + + #[test] + fn missing_name_and_age() { + assert!(matches!( + ",".parse::(), + Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), + )); + } + + #[test] + fn missing_name_and_invalid_age() { + assert!(matches!( + ",one".parse::(), + Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), + )); + } + + #[test] + fn trailing_comma() { + assert_eq!("John,32,".parse::(), Err(ParsePersonError::BadLen)); + } + + #[test] + fn trailing_comma_and_some_string() { + assert_eq!( + "John,32,man".parse::(), + Err(ParsePersonError::BadLen), + ); + } +} From 5217cdc5e2c49d179497e5ef65d0dc8bff1e0950 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:26:09 +0200 Subject: [PATCH 420/433] try_from_into solution --- exercises/23_conversions/try_from_into.rs | 113 ++++++------- rustlings-macros/info.toml | 17 +- solutions/23_conversions/try_from_into.rs | 193 +++++++++++++++++++++- 3 files changed, 246 insertions(+), 77 deletions(-) diff --git a/exercises/23_conversions/try_from_into.rs b/exercises/23_conversions/try_from_into.rs index da45e5a4..f3ae80a9 100644 --- a/exercises/23_conversions/try_from_into.rs +++ b/exercises/23_conversions/try_from_into.rs @@ -1,9 +1,10 @@ -// TryFrom is a simple and safe type conversion that may fail in a controlled -// way under some circumstances. Basically, this is the same as From. The main -// difference is that this should return a Result type instead of the target -// type itself. You can read more about it at +// `TryFrom` is a simple and safe type conversion that may fail in a controlled +// way under some circumstances. Basically, this is the same as `From`. The main +// difference is that this should return a `Result` type instead of the target +// type itself. You can read more about it in the documentation: // https://doc.rust-lang.org/std/convert/trait.TryFrom.html +#![allow(clippy::useless_vec)] use std::convert::{TryFrom, TryInto}; #[derive(Debug, PartialEq)] @@ -13,7 +14,7 @@ struct Color { blue: u8, } -// We will use this error type for these `TryFrom` conversions. +// We will use this error type for the `TryFrom` conversions. #[derive(Debug, PartialEq)] enum IntoColorError { // Incorrect length of slice @@ -22,78 +23,67 @@ enum IntoColorError { IntConversion, } -// Your task is to complete this implementation and return an Ok result of inner -// type Color. You need to create an implementation for a tuple of three -// integers, an array of three integers, and a slice of integers. -// -// Note that the implementation for tuple and array will be checked at compile -// time, but the slice implementation needs to check the slice length! Also note -// that correct RGB color values must be integers in the 0..=255 range. - -// Tuple implementation +// TODO: Tuple implementation. +// Correct RGB color values must be integers in the 0..=255 range. impl TryFrom<(i16, i16, i16)> for Color { type Error = IntoColorError; - fn try_from(tuple: (i16, i16, i16)) -> Result { - } + + fn try_from(tuple: (i16, i16, i16)) -> Result {} } -// Array implementation +// TODO: Array implementation. impl TryFrom<[i16; 3]> for Color { type Error = IntoColorError; - fn try_from(arr: [i16; 3]) -> Result { - } + + fn try_from(arr: [i16; 3]) -> Result {} } -// Slice implementation +// TODO: Slice implementation. +// This implementation needs to check the slice length. impl TryFrom<&[i16]> for Color { type Error = IntoColorError; - fn try_from(slice: &[i16]) -> Result { - } + + fn try_from(slice: &[i16]) -> Result {} } fn main() { - // Use the `try_from` function + // Using the `try_from` function. let c1 = Color::try_from((183, 65, 14)); - println!("{:?}", c1); + println!("{c1:?}"); - // Since TryFrom is implemented for Color, we should be able to use TryInto + // Since `TryFrom` is implemented for `Color`, we can use `TryInto`. let c2: Result = [183, 65, 14].try_into(); - println!("{:?}", c2); + println!("{c2:?}"); let v = vec![183, 65, 14]; - // With slice we should use `try_from` function + // With slice we should use the `try_from` function let c3 = Color::try_from(&v[..]); - println!("{:?}", c3); - // or take slice within round brackets and use TryInto + println!("{c3:?}"); + // or put the slice within round brackets and use `try_into`. let c4: Result = (&v[..]).try_into(); - println!("{:?}", c4); + println!("{c4:?}"); } #[cfg(test)] mod tests { use super::*; + use IntoColorError::*; #[test] fn test_tuple_out_of_range_positive() { - assert_eq!( - Color::try_from((256, 1000, 10000)), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion)); } + #[test] fn test_tuple_out_of_range_negative() { - assert_eq!( - Color::try_from((-1, -10, -256)), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion)); } + #[test] fn test_tuple_sum() { - assert_eq!( - Color::try_from((-1, 255, 255)), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from((-1, 255, 255)), Err(IntConversion)); } + #[test] fn test_tuple_correct() { let c: Result = (183, 65, 14).try_into(); @@ -103,25 +93,29 @@ mod tests { Color { red: 183, green: 65, - blue: 14 + blue: 14, } ); } + #[test] fn test_array_out_of_range_positive() { let c: Result = [1000, 10000, 256].try_into(); - assert_eq!(c, Err(IntoColorError::IntConversion)); + assert_eq!(c, Err(IntConversion)); } + #[test] fn test_array_out_of_range_negative() { let c: Result = [-10, -256, -1].try_into(); - assert_eq!(c, Err(IntoColorError::IntConversion)); + assert_eq!(c, Err(IntConversion)); } + #[test] fn test_array_sum() { let c: Result = [-1, 255, 255].try_into(); - assert_eq!(c, Err(IntoColorError::IntConversion)); + assert_eq!(c, Err(IntConversion)); } + #[test] fn test_array_correct() { let c: Result = [183, 65, 14].try_into(); @@ -135,30 +129,25 @@ mod tests { } ); } + #[test] fn test_slice_out_of_range_positive() { let arr = [10000, 256, 1000]; - assert_eq!( - Color::try_from(&arr[..]), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); } + #[test] fn test_slice_out_of_range_negative() { let arr = [-256, -1, -10]; - assert_eq!( - Color::try_from(&arr[..]), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); } + #[test] fn test_slice_sum() { let arr = [-1, 255, 255]; - assert_eq!( - Color::try_from(&arr[..]), - Err(IntoColorError::IntConversion) - ); + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); } + #[test] fn test_slice_correct() { let v = vec![183, 65, 14]; @@ -169,18 +158,20 @@ mod tests { Color { red: 183, green: 65, - blue: 14 + blue: 14, } ); } + #[test] fn test_slice_excess_length() { let v = vec![0, 0, 0, 0]; - assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen)); + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); } + #[test] fn test_slice_insufficient_length() { let v = vec![0, 0]; - assert_eq!(Color::try_from(&v[..]), Err(IntoColorError::BadLen)); + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); } } diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 4ef1a0a3..488fdacf 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1197,21 +1197,8 @@ https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reen name = "try_from_into" dir = "23_conversions" hint = """ -Follow the steps provided right before the `TryFrom` implementation. -You can also use the example at -https://doc.rust-lang.org/std/convert/trait.TryFrom.html - -Is there an implementation of `TryFrom` in the standard library that -can both do the required integer conversion and check the range of the input? - -Another hint: Look at the test cases to see which error variants to return. - -Yet another hint: You can use the `map_err` or `or` methods of `Result` to -convert errors. - -Yet another hint: If you would like to propagate errors by using the `?` -operator in your solution, you might want to look at -https://doc.rust-lang.org/stable/rust-by-example/error/multiple_error_types/reenter_question_mark.html +Is there an implementation of `TryFrom` in the standard library that can both do +the required integer conversion and check the range of the input? Challenge: Can you make the `TryFrom` implementations generic over many integer types?""" diff --git a/solutions/23_conversions/try_from_into.rs b/solutions/23_conversions/try_from_into.rs index 4e181989..acb7721d 100644 --- a/solutions/23_conversions/try_from_into.rs +++ b/solutions/23_conversions/try_from_into.rs @@ -1 +1,192 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// `TryFrom` is a simple and safe type conversion that may fail in a controlled +// way under some circumstances. Basically, this is the same as `From`. The main +// difference is that this should return a `Result` type instead of the target +// type itself. You can read more about it in the documentation: +// https://doc.rust-lang.org/std/convert/trait.TryFrom.html + +use std::convert::{TryFrom, TryInto}; + +#[derive(Debug, PartialEq)] +struct Color { + red: u8, + green: u8, + blue: u8, +} + +// We will use this error type for the `TryFrom` conversions. +#[derive(Debug, PartialEq)] +enum IntoColorError { + // Incorrect length of slice + BadLen, + // Integer conversion error + IntConversion, +} + +impl TryFrom<(i16, i16, i16)> for Color { + type Error = IntoColorError; + + fn try_from(tuple: (i16, i16, i16)) -> Result { + let (Ok(red), Ok(green), Ok(blue)) = ( + u8::try_from(tuple.0), + u8::try_from(tuple.1), + u8::try_from(tuple.2), + ) else { + return Err(IntoColorError::IntConversion); + }; + + Ok(Self { red, green, blue }) + } +} + +impl TryFrom<[i16; 3]> for Color { + type Error = IntoColorError; + + fn try_from(arr: [i16; 3]) -> Result { + // Reuse the implementation for a tuple. + Self::try_from((arr[0], arr[1], arr[2])) + } +} + +impl TryFrom<&[i16]> for Color { + type Error = IntoColorError; + + fn try_from(slice: &[i16]) -> Result { + // Check the length. + if slice.len() != 3 { + return Err(IntoColorError::BadLen); + } + + // Reuse the implementation for a tuple. + Self::try_from((slice[0], slice[1], slice[2])) + } +} + +fn main() { + // Using the `try_from` function. + let c1 = Color::try_from((183, 65, 14)); + println!("{c1:?}"); + + // Since `TryFrom` is implemented for `Color`, we can use `TryInto`. + let c2: Result = [183, 65, 14].try_into(); + println!("{c2:?}"); + + let v = vec![183, 65, 14]; + // With slice we should use the `try_from` function + let c3 = Color::try_from(&v[..]); + println!("{c3:?}"); + // or put the slice within round brackets and use `try_into`. + let c4: Result = (&v[..]).try_into(); + println!("{c4:?}"); +} + +#[cfg(test)] +mod tests { + use super::*; + use IntoColorError::*; + + #[test] + fn test_tuple_out_of_range_positive() { + assert_eq!(Color::try_from((256, 1000, 10000)), Err(IntConversion)); + } + + #[test] + fn test_tuple_out_of_range_negative() { + assert_eq!(Color::try_from((-1, -10, -256)), Err(IntConversion)); + } + + #[test] + fn test_tuple_sum() { + assert_eq!(Color::try_from((-1, 255, 255)), Err(IntConversion)); + } + + #[test] + fn test_tuple_correct() { + let c: Result = (183, 65, 14).try_into(); + assert!(c.is_ok()); + assert_eq!( + c.unwrap(), + Color { + red: 183, + green: 65, + blue: 14, + } + ); + } + + #[test] + fn test_array_out_of_range_positive() { + let c: Result = [1000, 10000, 256].try_into(); + assert_eq!(c, Err(IntConversion)); + } + + #[test] + fn test_array_out_of_range_negative() { + let c: Result = [-10, -256, -1].try_into(); + assert_eq!(c, Err(IntConversion)); + } + + #[test] + fn test_array_sum() { + let c: Result = [-1, 255, 255].try_into(); + assert_eq!(c, Err(IntConversion)); + } + + #[test] + fn test_array_correct() { + let c: Result = [183, 65, 14].try_into(); + assert!(c.is_ok()); + assert_eq!( + c.unwrap(), + Color { + red: 183, + green: 65, + blue: 14 + } + ); + } + + #[test] + fn test_slice_out_of_range_positive() { + let arr = [10000, 256, 1000]; + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); + } + + #[test] + fn test_slice_out_of_range_negative() { + let arr = [-256, -1, -10]; + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); + } + + #[test] + fn test_slice_sum() { + let arr = [-1, 255, 255]; + assert_eq!(Color::try_from(&arr[..]), Err(IntConversion)); + } + + #[test] + fn test_slice_correct() { + let v = vec![183, 65, 14]; + let c: Result = Color::try_from(&v[..]); + assert!(c.is_ok()); + assert_eq!( + c.unwrap(), + Color { + red: 183, + green: 65, + blue: 14, + } + ); + } + + #[test] + fn test_slice_excess_length() { + let v = vec![0, 0, 0, 0]; + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); + } + + #[test] + fn test_slice_insufficient_length() { + let v = vec![0, 0]; + assert_eq!(Color::try_from(&v[..]), Err(BadLen)); + } +} From 8ef5d10da2252ec270b9170af25dabc466113e99 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:29:30 +0200 Subject: [PATCH 421/433] Import the error variants in the tests --- exercises/23_conversions/from_str.rs | 31 +++++++++------------------- solutions/23_conversions/from_str.rs | 31 +++++++++------------------- 2 files changed, 20 insertions(+), 42 deletions(-) diff --git a/exercises/23_conversions/from_str.rs b/exercises/23_conversions/from_str.rs index 1b3f553e..4b1aaa28 100644 --- a/exercises/23_conversions/from_str.rs +++ b/exercises/23_conversions/from_str.rs @@ -52,10 +52,11 @@ fn main() { #[cfg(test)] mod tests { use super::*; + use ParsePersonError::*; #[test] fn empty_input() { - assert_eq!("".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("".parse::(), Err(BadLen)); } #[test] @@ -69,56 +70,44 @@ mod tests { #[test] fn missing_age() { - assert!(matches!( - "John,".parse::(), - Err(ParsePersonError::ParseInt(_)), - )); + assert!(matches!("John,".parse::(), Err(ParseInt(_)))); } #[test] fn invalid_age() { - assert!(matches!( - "John,twenty".parse::(), - Err(ParsePersonError::ParseInt(_)), - )); + assert!(matches!("John,twenty".parse::(), Err(ParseInt(_)))); } #[test] fn missing_comma_and_age() { - assert_eq!("John".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("John".parse::(), Err(BadLen)); } #[test] fn missing_name() { - assert_eq!(",1".parse::(), Err(ParsePersonError::NoName)); + assert_eq!(",1".parse::(), Err(NoName)); } #[test] fn missing_name_and_age() { - assert!(matches!( - ",".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), - )); + assert!(matches!(",".parse::(), Err(NoName | ParseInt(_)))); } #[test] fn missing_name_and_invalid_age() { assert!(matches!( ",one".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), + Err(NoName | ParseInt(_)), )); } #[test] fn trailing_comma() { - assert_eq!("John,32,".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("John,32,".parse::(), Err(BadLen)); } #[test] fn trailing_comma_and_some_string() { - assert_eq!( - "John,32,man".parse::(), - Err(ParsePersonError::BadLen), - ); + assert_eq!("John,32,man".parse::(), Err(BadLen)); } } diff --git a/solutions/23_conversions/from_str.rs b/solutions/23_conversions/from_str.rs index 301150b9..005b5012 100644 --- a/solutions/23_conversions/from_str.rs +++ b/solutions/23_conversions/from_str.rs @@ -56,10 +56,11 @@ fn main() { #[cfg(test)] mod tests { use super::*; + use ParsePersonError::*; #[test] fn empty_input() { - assert_eq!("".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("".parse::(), Err(BadLen)); } #[test] @@ -73,56 +74,44 @@ mod tests { #[test] fn missing_age() { - assert!(matches!( - "John,".parse::(), - Err(ParsePersonError::ParseInt(_)), - )); + assert!(matches!("John,".parse::(), Err(ParseInt(_)))); } #[test] fn invalid_age() { - assert!(matches!( - "John,twenty".parse::(), - Err(ParsePersonError::ParseInt(_)), - )); + assert!(matches!("John,twenty".parse::(), Err(ParseInt(_)))); } #[test] fn missing_comma_and_age() { - assert_eq!("John".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("John".parse::(), Err(BadLen)); } #[test] fn missing_name() { - assert_eq!(",1".parse::(), Err(ParsePersonError::NoName)); + assert_eq!(",1".parse::(), Err(NoName)); } #[test] fn missing_name_and_age() { - assert!(matches!( - ",".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), - )); + assert!(matches!(",".parse::(), Err(NoName | ParseInt(_)))); } #[test] fn missing_name_and_invalid_age() { assert!(matches!( ",one".parse::(), - Err(ParsePersonError::NoName | ParsePersonError::ParseInt(_)), + Err(NoName | ParseInt(_)), )); } #[test] fn trailing_comma() { - assert_eq!("John,32,".parse::(), Err(ParsePersonError::BadLen)); + assert_eq!("John,32,".parse::(), Err(BadLen)); } #[test] fn trailing_comma_and_some_string() { - assert_eq!( - "John,32,man".parse::(), - Err(ParsePersonError::BadLen), - ); + assert_eq!("John,32,man".parse::(), Err(BadLen)); } } From 825637f32c652a4ca9fb59ba0cf7b2191dacacc9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:35:38 +0200 Subject: [PATCH 422/433] as_ref_mut solution --- exercises/23_conversions/as_ref_mut.rs | 7 ++- solutions/23_conversions/as_ref_mut.rs | 60 +++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/exercises/23_conversions/as_ref_mut.rs b/exercises/23_conversions/as_ref_mut.rs index c725dfde..54f0cd11 100644 --- a/exercises/23_conversions/as_ref_mut.rs +++ b/exercises/23_conversions/as_ref_mut.rs @@ -3,22 +3,21 @@ // https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. // Obtain the number of bytes (not characters) in the given argument. -// TODO: Add the AsRef trait appropriately as a trait bound. +// TODO: Add the `AsRef` trait appropriately as a trait bound. fn byte_counter(arg: T) -> usize { arg.as_ref().as_bytes().len() } // Obtain the number of characters (not bytes) in the given argument. -// TODO: Add the AsRef trait appropriately as a trait bound. +// TODO: Add the `AsRef` trait appropriately as a trait bound. fn char_counter(arg: T) -> usize { arg.as_ref().chars().count() } -// Squares a number using as_mut(). +// Squares a number using `as_mut()`. // TODO: Add the appropriate trait bound. fn num_sq(arg: &mut T) { // TODO: Implement the function body. - ??? } fn main() { diff --git a/solutions/23_conversions/as_ref_mut.rs b/solutions/23_conversions/as_ref_mut.rs index 4e181989..91b12bac 100644 --- a/solutions/23_conversions/as_ref_mut.rs +++ b/solutions/23_conversions/as_ref_mut.rs @@ -1 +1,59 @@ -// Solutions will be available before the stable release. Thank you for testing the beta version 🥰 +// AsRef and AsMut allow for cheap reference-to-reference conversions. Read more +// about them at https://doc.rust-lang.org/std/convert/trait.AsRef.html and +// https://doc.rust-lang.org/std/convert/trait.AsMut.html, respectively. + +// Obtain the number of bytes (not characters) in the given argument. +fn byte_counter>(arg: T) -> usize { + arg.as_ref().as_bytes().len() +} + +// Obtain the number of characters (not bytes) in the given argument. +fn char_counter>(arg: T) -> usize { + arg.as_ref().chars().count() +} + +// Squares a number using `as_mut()`. +fn num_sq>(arg: &mut T) { + let arg = arg.as_mut(); + *arg = *arg * *arg; +} + +fn main() { + // You can optionally experiment here. +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn different_counts() { + let s = "Café au lait"; + assert_ne!(char_counter(s), byte_counter(s)); + } + + #[test] + fn same_counts() { + let s = "Cafe au lait"; + assert_eq!(char_counter(s), byte_counter(s)); + } + + #[test] + fn different_counts_using_string() { + let s = String::from("Café au lait"); + assert_ne!(char_counter(s.clone()), byte_counter(s)); + } + + #[test] + fn same_counts_using_string() { + let s = String::from("Cafe au lait"); + assert_eq!(char_counter(s.clone()), byte_counter(s)); + } + + #[test] + fn mut_box() { + let mut num: Box = Box::new(3); + num_sq(&mut num); + assert_eq!(*num, 9); + } +} From bcebbb9df6987cd21ecf615907d5d06f8b626edc Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:45:55 +0200 Subject: [PATCH 423/433] Update deps --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f41ba86e..618e422f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstream", "anstyle", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", @@ -416,9 +416,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lru" @@ -729,9 +729,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 64844c3a..97f1c194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,14 +47,14 @@ include = [ [dependencies] anyhow = "1.0.86" -clap = { version = "4.5.7", features = ["derive"] } +clap = { version = "4.5.8", features = ["derive"] } crossterm = "0.27.0" hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.0" ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" } -serde_json = "1.0.118" +serde_json = "1.0.120" serde.workspace = true toml_edit.workspace = true From 67ce9b9e56b1d9e8b8b50b9b48c7eebd80b9ec8f Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 01:50:05 +0200 Subject: [PATCH 424/433] Underline "next" --- src/watch/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/watch/state.rs b/src/watch/state.rs index dd43c566..78af30a4 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -101,7 +101,7 @@ impl<'a> WatchState<'a> { } if self.done_status != DoneStatus::Pending { - write!(self.writer, "{}:next / ", 'n'.bold())?; + write!(self.writer, "{}:{} / ", 'n'.bold(), "next".underlined())?; } if !self.show_hint { From 6cf75d569bd0dd33a041e37c59cb75d28664bd7b Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 14:28:08 +0200 Subject: [PATCH 425/433] Fix typos --- exercises/15_traits/traits2.rs | 2 +- exercises/20_threads/threads3.rs | 2 +- src/dev/check.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/exercises/15_traits/traits2.rs b/exercises/15_traits/traits2.rs index e9040162..d724dc28 100644 --- a/exercises/15_traits/traits2.rs +++ b/exercises/15_traits/traits2.rs @@ -3,7 +3,7 @@ trait AppendBar { } // TODO: Implement the trait `AppendBar` for a vector of strings. -// `appned_bar` should push the string "Bar" into the vector. +// `append_bar` should push the string "Bar" into the vector. fn main() { // You can optionally experiment here. diff --git a/exercises/20_threads/threads3.rs b/exercises/20_threads/threads3.rs index 30ac8ddd..8aa7291f 100644 --- a/exercises/20_threads/threads3.rs +++ b/exercises/20_threads/threads3.rs @@ -18,7 +18,7 @@ impl Queue { fn send_tx(q: Queue, tx: mpsc::Sender) { // TODO: We want to send `tx` to both threads. But currently, it is moved - // into the frist thread. How could you solve this problem? + // into the first thread. How could you solve this problem? thread::spawn(move || { for val in q.first_half { println!("Sending {val:?}"); diff --git a/src/dev/check.rs b/src/dev/check.rs index 336360be..5074c133 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -174,7 +174,7 @@ fn check_exercises(info_file: &InfoFile) -> Result<()> { fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> { let target_dir = parse_target_dir()?; let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len())); - let error_occured = AtomicBool::new(false); + let error_occurred = AtomicBool::new(false); println!("Running all solutions. This may take a while...\n"); thread::scope(|s| { @@ -188,7 +188,7 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> .unwrap(); stderr.write_all(exercise_info.name.as_bytes()).unwrap(); stderr.write_all(SEPARATOR).unwrap(); - error_occured.store(true, atomic::Ordering::Relaxed); + error_occurred.store(true, atomic::Ordering::Relaxed); }; let path = exercise_info.sol_path(); @@ -213,7 +213,7 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile) -> Result<()> } }); - if error_occured.load(atomic::Ordering::Relaxed) { + if error_occurred.load(atomic::Ordering::Relaxed) { bail!("At least one solution failed. See the output above."); } From 2d792651ea5d500537e23d2825b919cff769d30b Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 14:29:07 +0200 Subject: [PATCH 426/433] chore: Release --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 618e422f..f5908cc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,7 +654,7 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rustlings" -version = "6.0.0-beta.9" +version = "6.0.0-beta.10" dependencies = [ "anyhow", "assert_cmd", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "rustlings-macros" -version = "6.0.0-beta.9" +version = "6.0.0-beta.10" dependencies = [ "quote", "serde", diff --git a/Cargo.toml b/Cargo.toml index 97f1c194..3455e2b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ exclude = [ ] [workspace.package] -version = "6.0.0-beta.9" +version = "6.0.0-beta.10" authors = [ "Liv ", "Mo Bitar ", @@ -53,7 +53,7 @@ hashbrown = "0.14.5" notify-debouncer-mini = { version = "0.4.1", default-features = false } os_pipe = "1.2.0" ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } -rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.9" } +rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.10" } serde_json = "1.0.120" serde.workspace = true toml_edit.workspace = true From 43eb014026e1c719bd0926cd2cb5fc7b8a18ae1f Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 14:45:19 +0200 Subject: [PATCH 427/433] Update README --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c0de89d3..9463038d 100644 --- a/README.md +++ b/README.md @@ -32,19 +32,15 @@ This'll also install _Cargo_, Rust's package/project manager. The following command will download and compile Rustlings: - - ```bash -cargo install rustlings@6.0.0-beta.9 +cargo install rustlings ```
If the installation fails… (click to expand) - - - Make sure you have the latest Rust version by running `rustup update` -- Try adding the `--locked` flag: `cargo install rustlings@6.0.0-beta.9 --locked` +- Try adding the `--locked` flag: `cargo install rustlings --locked` - Otherwise, please [report the issue](https://github.com/rust-lang/rustlings/issues/new)
@@ -88,6 +84,9 @@ We highly recommend that you have a look at them before you start 📚️ Most exercises contain an error that keeps them from compiling, and it's up to you to fix it! Some exercises contain tests that need to pass for the exercise to be done ✅ +Search for `TODO` and `todo!()` to find out what you need to change. +Ask for hints by entering `h` in the _watch mode_ 💡 + ### Watch Mode After [initialization](#initialization), Rustlings can be launched by simply running the command `rustlings`. From 4c5573b09f13221572867441618442bf1fef66d7 Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 14:45:25 +0200 Subject: [PATCH 428/433] Update CONTRIBUTING --- CONTRIBUTING.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc00a6b0..95605f70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,11 +32,14 @@ If you need any help with it or face any Git related problems, don't hesitate to It may take time to review your pull request. Please be patient 😇 +When updating an exercise, check if its solution needs to be updated. + ## Adding An Exercise - Name the file `exercises/yourTopic/yourTopicN.rs`. -- Make sure to put in some helpful links, and link to sections of the book in `exercises/yourTopic/README.md`. -- Add a (possible) solution at `solutions/yourTopic/yourTopicN.rs` with comments and links explaining it. +- Make sure to put in some helpful links, and link to sections of The Book in `exercises/yourTopic/README.md`. +- In the exercise, add a `// TODO: …` comment where user changes are required. +- Add a solution at `solutions/yourTopic/yourTopicN.rs` with comments explaining it. - Add the [metadata for your exercise](#exercise-metadata) in the `rustlings-macros/info.toml` file. - Make sure your exercise runs with `rustlings run yourTopicN`. - [Open a pull request](#pull-requests). @@ -49,7 +52,9 @@ The exercise metadata should contain the following: [[exercises]] name = "yourTopicN" dir = "yourTopic" -hint = """A useful (multi-line) hint for your exercise.""" +hint = """ +A useful (multi-line) hint for your exercise. +Include links to a section in The Book or a documentation page.""" ``` If your exercise doesn't contain any test, add `test = false` to the exercise metadata. From 9bb174e96ef1f719cf946134e7e40fb9f5b331dc Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 16:09:05 +0200 Subject: [PATCH 429/433] Add a guide for third-party exercises --- README.md | 8 ++++-- THIRD_PARTY_EXERCISES.md | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 THIRD_PARTY_EXERCISES.md diff --git a/README.md b/README.md index 9463038d..4cee71ce 100644 --- a/README.md +++ b/README.md @@ -117,11 +117,15 @@ See the footer of the list for all possible keys. ## Continuing On - - Once you've completed Rustlings, put your new knowledge to good use! Continue practicing your Rust skills by building your own projects, contributing to Rustlings, or finding other open-source projects to contribute to. +## Third-Party Exercises + +Do you want to create your own set of Rustlings exercises to focus on some specific topic? +Or did you want to translate the original Rustlings exercises? +Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! + ## Uninstalling Rustlings If you want to remove Rustlings from your system, run the following command: diff --git a/THIRD_PARTY_EXERCISES.md b/THIRD_PARTY_EXERCISES.md new file mode 100644 index 00000000..2ae8b722 --- /dev/null +++ b/THIRD_PARTY_EXERCISES.md @@ -0,0 +1,53 @@ +# Third-Party Exercises + +The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic. +You could also offer a translatation of the original Rustlings exercises as a third-party exercises. + +## Getting started + +To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`. +This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started. + +Read the comments in the generated `info.toml` file to understand the format of this file. +It allows you to set a custom welcome and final message and specify the metadata of every exercise. + +## Create an exercise + +Here is an example of the metadata of one file: + +```toml +[[exercises]] +name = "intro1" +hint = """ +To finish this exercise, you need to … +This link might help you …""" +``` + +After entering this in `info.toml`, create the file `intro1.rs` in the `exercises/` directory. +The exercise needs to contain a `main` function, but it can be empty. +Adding tests is recommended. +Look at the official Rustlings exercises for inspiration. + +You can optionally add a solution file `intro1.rs` to the `solutions/` directory. + +Now, run `rustlings dev check`. +It will tell you about any issues with your exercises. +For example, it will tell you to run `rustlings dev update` to update the `Cargo.toml` file to include the new exercise `intro1`. + +`rustlings dev check` will also run your solutions (if you have any) to make sure that they run successfully. + +That's it! +You finished your first exercise 🎉 + +## Publish + +Now, add more exercises and publish them as a Git repository. + +Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones. + +One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise. +But you can trust the user to not look at the solution too early ;) + +## Share + +After publishing your set of exercises, open a pull request in the official Rustlings repository to link to your project in the README 😃 From 95f10c80689db6b156ced7fa43bd56309137cc7f Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 16:10:42 +0200 Subject: [PATCH 430/433] Update CHANGELOG --- CHANGELOG.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a199e4de..38c5b203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ + +## 6.0.0 (2024-07-02) + +This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨ +The most important changes are highlighted below. + +### Installation + +The installation has been simplified a lot! +To install Rustlings after installing Rust, all what you need to do now is running the following command: + +```bash +cargo install rustlings +``` + +Yes, this means that Rustlings is now on [crates.io](https://crates.io/crates/rustlings) 🎉 + +You can read about the motivations of this change in [this issue](https://github.com/rust-lang/rustlings/issues/1919). + +### UI/UX + +- The UI is now responsive when the terminal is resized. +- The progress bar was moved to the bottom so that you can always see your progress and the current exercise to work on. +- The current exercise path is now a terminal link. It will open the exercise file in your default editor when you click on it. +- A small prompt is now always shown at the bottom. It allows you to choose an action by entering a character. For example, entering `h` will show you the hint of the current exercise. +- The comment "I AM NOT DONE!" doesn't exist anymore. Instead of needing to remove it to go to the next exercise, you need to enter `n` in the terminal. + +### List mode + +A list mode was added using [`Ratatui`](https://ratatui.rs). +You can enter it by entering `l` in the watch mode. +It offers the following features: + +- Browse all exercises and see their state (pending/done). +- Filter exercises based on their state (done/pending). +- Continue at another exercise. This allows you to skip some exercises or go back to previous ones. +- Reset an exercise so that you can start over and revert your changes. + +### Solutions + +After finishing an exercise, a solution file will be available and Rustlings will show you its path in green. +This allows you to compare your solution with an idiomatic solution and maybe learn about other ways to solve a problem. + +### LSP support out of the box + +Instead of creating a `project.json` file using `rustlings lsp`, Rustlings now works with a `Cargo.toml` file. +This should avoid issues related to the language server or to running exercises, especially the ones with Clippy. + +### Clippy + +Clippy lints are now shown on all exercises! 📎 +Make Clippy your friend from early on 🥰 + +### Third party exercises + +Rustlings now supports third-party exercises! + +Do you want to create your own set of Rustlings exercises to focus on some specific topic? +Or did you want to translate the original Rustlings exercises? +Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! + ## 5.6.1 (2023-09-18) @@ -30,7 +91,7 @@ - Swapped the order of threads and smart pointer exercises. - Rewrote the CLI to use `clap` - it's matured much since we switched to `argh` :) - `structs3`: Switched from i32 to u32. -- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same +- `move_semantics`: Switched 1-4 to tests, and rewrote them to be way simpler, while still teaching about the same concepts. #### Fixed From d3a0c269994eb2b11c0a3418e4db3a275a7ee5ad Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 16:26:28 +0200 Subject: [PATCH 431/433] Improve the placement of TODO comments --- exercises/00_intro/intro1.rs | 4 ++-- exercises/00_intro/intro2.rs | 3 +-- exercises/01_variables/variables4.rs | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/exercises/00_intro/intro1.rs b/exercises/00_intro/intro1.rs index bdbf34b0..22544cd4 100644 --- a/exercises/00_intro/intro1.rs +++ b/exercises/00_intro/intro1.rs @@ -1,5 +1,5 @@ -// We sometimes encourage you to keep trying things on a given exercise, even -// after you already figured it out. If you got everything working and feel +// TODO: We sometimes encourage you to keep trying things on a given exercise, +// even after you already figured it out. If you got everything working and feel // ready for the next exercise, enter `n` in the terminal. // // The exercise file will be reloaded when you change one of the lines below! diff --git a/exercises/00_intro/intro2.rs b/exercises/00_intro/intro2.rs index e443ec8f..c6cb6451 100644 --- a/exercises/00_intro/intro2.rs +++ b/exercises/00_intro/intro2.rs @@ -1,5 +1,4 @@ -// TODO: Fix the code to print "Hello world!". - fn main() { + // TODO: Fix the code to print "Hello world!". printline!("Hello world!"); } diff --git a/exercises/01_variables/variables4.rs b/exercises/01_variables/variables4.rs index 8634ceb5..6c138b18 100644 --- a/exercises/01_variables/variables4.rs +++ b/exercises/01_variables/variables4.rs @@ -1,5 +1,4 @@ // TODO: Fix the compiler error. - fn main() { let x = 3; println!("Number {x}"); From 2f8fa469ac9ce6b3ccff956215cdaacc16e9dbab Mon Sep 17 00:00:00 2001 From: mo8it Date: Tue, 2 Jul 2024 16:26:59 +0200 Subject: [PATCH 432/433] Small writing changes --- CHANGELOG.md | 15 ++++++++++----- README.md | 2 +- THIRD_PARTY_EXERCISES.md | 8 ++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c5b203..5cc4c980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,23 +32,28 @@ You can enter it by entering `l` in the watch mode. It offers the following features: - Browse all exercises and see their state (pending/done). -- Filter exercises based on their state (done/pending). +- Filter exercises based on their state (pending/done). - Continue at another exercise. This allows you to skip some exercises or go back to previous ones. -- Reset an exercise so that you can start over and revert your changes. +- Reset an exercise so you can start over and revert your changes. ### Solutions After finishing an exercise, a solution file will be available and Rustlings will show you its path in green. This allows you to compare your solution with an idiomatic solution and maybe learn about other ways to solve a problem. +While writing the solutions, all exercises have been polished 🌟 +For example, every exercise now contains `TODO` comments to highlight what the user needs to change and where. + ### LSP support out of the box -Instead of creating a `project.json` file using `rustlings lsp`, Rustlings now works with a `Cargo.toml` file. +Instead of creating a `project.json` file using `rustlings lsp`, Rustlings now works with a `Cargo.toml` file out of the box. +No actions are needed to activate the language server `rust-analyzer`. + This should avoid issues related to the language server or to running exercises, especially the ones with Clippy. ### Clippy -Clippy lints are now shown on all exercises! 📎 +Clippy lints are now shown on all exercises, not only the Clippy exercises 📎 Make Clippy your friend from early on 🥰 ### Third party exercises @@ -56,7 +61,7 @@ Make Clippy your friend from early on 🥰 Rustlings now supports third-party exercises! Do you want to create your own set of Rustlings exercises to focus on some specific topic? -Or did you want to translate the original Rustlings exercises? +Or do you want to translate the original Rustlings exercises? Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! diff --git a/README.md b/README.md index 4cee71ce..373b9c79 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Continue practicing your Rust skills by building your own projects, contributing ## Third-Party Exercises Do you want to create your own set of Rustlings exercises to focus on some specific topic? -Or did you want to translate the original Rustlings exercises? +Or do you want to translate the original Rustlings exercises? Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! ## Uninstalling Rustlings diff --git a/THIRD_PARTY_EXERCISES.md b/THIRD_PARTY_EXERCISES.md index 2ae8b722..5c066941 100644 --- a/THIRD_PARTY_EXERCISES.md +++ b/THIRD_PARTY_EXERCISES.md @@ -1,14 +1,14 @@ # Third-Party Exercises The support of Rustlings for third-party exercises allows you to create your own set of Rustlings exercises to focus on some specific topic. -You could also offer a translatation of the original Rustlings exercises as a third-party exercises. +You could also offer a translatation of the original Rustlings exercises as third-party exercises. ## Getting started To create third-party exercises, install Rustlings and run `rustlings dev new PROJECT_NAME`. This command will, similar to `cargo new PROJECT_NAME`, create a template directory called `PROJECT_NAME` with all what you need to get started. -Read the comments in the generated `info.toml` file to understand the format of this file. +Read the comments in the generated `info.toml` file to understand its format. It allows you to set a custom welcome and final message and specify the metadata of every exercise. ## Create an exercise @@ -46,8 +46,8 @@ Now, add more exercises and publish them as a Git repository. Users just have to clone that repository and run `rustlings` in it to start working on your set of exercises just like the official ones. One difference to the official exercises is that the solution files will not be hidden until the user finishes an exercise. -But you can trust the user to not look at the solution too early ;) +But you can trust the users to not look at the solution too early 😉 ## Share -After publishing your set of exercises, open a pull request in the official Rustlings repository to link to your project in the README 😃 +After publishing your set of exercises, open an issue or a pull request in the official Rustlings repository to link to your project in the README 😃 From 33dfe5331a034a2705c45e420f52983a99e204f5 Mon Sep 17 00:00:00 2001 From: mo8it Date: Wed, 3 Jul 2024 15:24:07 +0200 Subject: [PATCH 433/433] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc4c980..11502ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ -## 6.0.0 (2024-07-02) + +## 6.0.0 (2024-07-03) This release is the result of a complete rewrite to deliver a ton of new features and improvements ✨ The most important changes are highlighted below. @@ -27,7 +28,7 @@ You can read about the motivations of this change in [this issue](https://github ### List mode -A list mode was added using [`Ratatui`](https://ratatui.rs). +A list mode was added using [Ratatui](https://ratatui.rs). You can enter it by entering `l` in the watch mode. It offers the following features: @@ -65,6 +66,7 @@ Or do you want to translate the original Rustlings exercises? Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXERCISES.md)! + ## 5.6.1 (2023-09-18) #### Changed @@ -81,6 +83,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - `enums3`: Fixed formatting with `rustfmt`. + ## 5.6.0 (2023-09-04) #### Added @@ -121,6 +124,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Improved CI workflows, we're now testing on multiple platforms at once. + ## 5.5.1 (2023-05-17) #### Fixed @@ -128,6 +132,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Reverted `rust-project.json` path generation due to an upstream `rust-analyzer` fix. + ## 5.5.0 (2023-05-17) #### Added @@ -163,6 +168,7 @@ Then follow the link to the guide about [third-party exercises](THIRD_PARTY_EXER - Split quick installation section into two code blocks + ## 5.4.1 (2023-03-10) #### Changed