use anyhow::{Context, Error, Result, bail}; use serde::Deserialize; use std::{fs, io::ErrorKind}; use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; /// Deserialized from the `info.toml` file. #[derive(Deserialize)] pub struct ExerciseInfo { /// Exercise's unique name. pub name: &'static str, /// Exercise's directory name inside the `exercises/` directory. pub dir: Option<&'static str>, /// Run `cargo test` on the exercise. #[serde(default = "default_true")] pub test: bool, /// Deny all Clippy warnings. #[serde(default)] pub strict_clippy: bool, /// The exercise's hint to be shown to the user on request. pub hint: &'static str, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. #[serde(default)] pub skip_check_unsolved: bool, } #[inline] const fn default_true() -> bool { true } impl ExerciseInfo { /// Path to the exercise file starting with the `exercises/` directory. pub fn path(&self) -> String { 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 { // 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 } } impl RunnableExercise for ExerciseInfo { #[inline] fn name(&self) -> &str { self.name } #[inline] fn dir(&self) -> Option<&str> { self.dir } #[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 { /// For possible breaking changes in the future for community exercises. pub format_version: u8, /// Shown to users when starting with the exercises. pub welcome_message: Option<&'static str>, /// Shown to users after finishing all exercises. pub final_message: Option<&'static str>, /// List of all exercises. pub exercises: Vec, } impl InfoFile { /// Official exercises: Parse the embedded `info.toml` file. /// Community 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("info.toml") { Ok(file_content) => { // Remove `\r` on Windows. // Leaking is fine since the info file is used until the end of the program. let file_content = String::from_utf8(file_content.into_iter().filter(|c| *c != b'\r').collect()) .context("Failed to parse `info.toml` as UTF8")? .leak(); toml::de::from_str::(file_content) .context("Failed to parse the `info.toml` file")? } Err(e) => { if e.kind() == ErrorKind::NotFound { return toml::de::from_str(EMBEDDED_FILES.info_file) .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}"); } Ok(slf) } } const NO_EXERCISES_ERR: &str = "There are no exercises yet! Add at least one exercise before testing.";