Compare commits

...

16 commits

Author SHA1 Message Date
mo8it d322bcfcec Add description 2024-04-16 04:04:45 +02:00
mo8it 0ac5aa7af2 Fix typo 2024-04-16 04:00:42 +02:00
mo8it f9be652b3b Ready to publish 2024-04-16 03:56:08 +02:00
mo8it 932f6b53a9 Add myself to the list of authors :) 2024-04-16 03:47:09 +02:00
mo8it 4d9eb35ad7 Prepare for publishing the first alpha version 2024-04-16 03:46:04 +02:00
mo8it 86d716cf8a Add comment about keeping dependencies 2024-04-16 03:43:34 +02:00
mo8it 87db9129bc Add the mode field 2024-04-16 03:37:58 +02:00
mo8it 6566c5904f Tell about updating Cargo.toml 2024-04-16 03:35:23 +02:00
mo8it aa813fbce1 Update Cargo.toml on dev check 2024-04-16 03:30:28 +02:00
mo8it d1ebbaa6f6 Add format_version to test info.toml files 2024-04-16 03:18:22 +02:00
mo8it c07cf5bffe Fix typo 2024-04-16 03:18:06 +02:00
mo8it df448c069c Fix running dev commands 2024-04-16 03:15:14 +02:00
mo8it 25e7696565 Done dev init 2024-04-16 03:08:45 +02:00
mo8it 92777c0a44 Add the format version 2024-04-16 01:22:54 +02:00
mo8it 7ebc260924 Scetch the dev subcommand 2024-04-15 23:54:57 +02:00
mo8it f5eaa578b9 Update deps 2024-04-15 23:35:30 +02:00
13 changed files with 240 additions and 92 deletions

41
Cargo.lock generated
View file

@ -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",
@ -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",
]
@ -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",
]

View file

@ -11,10 +11,11 @@ members = [
]
[workspace.package]
version = "6.0.0"
version = "6.0.0-alpha.0"
authors = [
"Liv <mokou@fastmail.com>",
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>",
"Mo <mo8it@proton.me>",
]
license = "MIT"
edition = "2021"
@ -32,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
@ -39,8 +47,8 @@ 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" }
ratatui = "0.26.2"
rustlings-macros = { path = "rustlings-macros", version = "6.0.0-alpha.0" }
serde.workspace = true
toml_edit.workspace = true
which = "6.0.1"

View file

@ -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:

View file

@ -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

25
src/dev.rs Normal file
View file

@ -0,0 +1,25 @@
use anyhow::{Context, Result};
use clap::Subcommand;
use crate::info_file::InfoFile;
mod check;
mod init;
#[derive(Subcommand)]
pub enum DevCommands {
Init,
Check,
}
impl DevCommands {
pub fn run(self, info_file: InfoFile) -> Result<()> {
match self {
DevCommands::Init => init::init().context(INIT_ERR),
DevCommands::Check => check::check(info_file),
}
}
}
const INIT_ERR: &str = "Initialization failed.
After resolving the issue, delete the `rustlings` directory (if it was created) and try again";

18
src/dev/check.rs Normal file
View file

@ -0,0 +1,18 @@
use std::fs;
use anyhow::{Context, Result};
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`");
println!("\nEverything looks fine!");
Ok(())
}

106
src/dev/init.rs Normal file
View file

@ -0,0 +1,106 @@
use std::fs::{self, create_dir};
use anyhow::{Context, Result};
use crate::CURRENT_FORMAT_VERSION;
pub fn init() -> Result<()> {
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(
"rustlings/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 `rustlings/.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 = "???"
# 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 multi-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"]"#;

View file

@ -39,6 +39,7 @@ impl ExerciseInfo {
#[derive(Deserialize)]
pub struct InfoFile {
pub format_version: u8,
pub welcome_message: Option<String>,
pub final_message: Option<String>,
pub exercises: Vec<ExerciseInfo>,

View file

@ -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<()> {
pub fn cargo_toml(exercise_infos: &[ExerciseInfo]) -> Vec<u8> {
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,33 @@ 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`")?;
println!("{POST_INIT_MSG}");
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
@ -106,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.";

View file

@ -1,4 +1,4 @@
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use app_state::StateFileStatus;
use clap::{Parser, Subcommand};
use crossterm::{
@ -12,6 +12,7 @@ use std::{
};
mod app_state;
mod dev;
mod embedded;
mod exercise;
mod info_file;
@ -21,14 +22,9 @@ 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};
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)]
@ -61,6 +57,8 @@ enum Subcommands {
/// The name of the exercise
name: String,
},
#[command(subcommand)]
Dev(DevCommands),
}
fn main() -> Result<()> {
@ -70,10 +68,16 @@ fn main() -> Result<()> {
let info_file = InfoFile::parse()?;
if matches!(args.command, Some(Subcommands::Init)) {
init(&info_file.exercises).context("Initialization failed")?;
println!("{POST_INIT_MSG}");
return Ok(());
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");
}
Some(Subcommands::Dev(dev_command)) => return dev_command.run(info_file),
_ => (),
}
if !Path::new("exercises").is_dir() {
@ -122,22 +126,20 @@ 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)?,
}
}
}
// `Init` is handled above.
Some(Subcommands::Init) => (),
Some(Subcommands::Run { name }) => {
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 +152,8 @@ fn main() -> Result<()> {
app_state.set_current_exercise_by_name(&name)?;
println!("{}", app_state.current_exercise().hint);
}
// Handled in an earlier match.
Some(Subcommands::Init | Subcommands::Dev(_)) => (),
}
Ok(())
@ -159,10 +163,10 @@ 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 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 PRE_INIT_MSG: &str = r"
Welcome to...

View file

@ -1,3 +1,5 @@
format_version = 1
[[exercises]]
name = "compFailure"
mode = "run"

View file

@ -1,3 +1,5 @@
format_version = 1
[[exercises]]
name = "pending_exercise"
mode = "run"

View file

@ -1,3 +1,5 @@
format_version = 1
[[exercises]]
name = "compSuccess"
mode = "run"