2024-04-13 19:15:43 -04:00
|
|
|
use anyhow::{bail, Context, Error, Result};
|
|
|
|
use serde::Deserialize;
|
2024-04-23 18:58:52 -04:00
|
|
|
use std::{fs, io::ErrorKind};
|
|
|
|
|
2024-06-01 15:48:15 -04:00
|
|
|
use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise};
|
2024-05-12 19:25:38 -04:00
|
|
|
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Deserialized from the `info.toml` file.
|
2024-04-13 19:15:43 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct ExerciseInfo {
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Exercise's unique name.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub name: String,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Exercise's directory name inside the `exercises/` directory.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub dir: Option<String>,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Run `cargo test` on the exercise.
|
2024-07-04 15:12:57 -04:00
|
|
|
#[serde(default = "default_true")]
|
2024-04-24 21:25:45 -04:00
|
|
|
pub test: bool,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Deny all Clippy warnings.
|
2024-04-24 21:25:45 -04:00
|
|
|
#[serde(default)]
|
|
|
|
pub strict_clippy: bool,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// The exercise's hint to be shown to the user on request.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub hint: String,
|
2024-07-04 15:12:57 -04:00
|
|
|
/// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
|
|
|
|
#[serde(default)]
|
|
|
|
pub skip_check_unsolved: bool,
|
2024-04-13 19:15:43 -04:00
|
|
|
}
|
2024-05-13 16:02:45 -04:00
|
|
|
#[inline(always)]
|
2024-04-24 21:25:45 -04:00
|
|
|
const fn default_true() -> bool {
|
|
|
|
true
|
|
|
|
}
|
2024-04-13 19:15:43 -04:00
|
|
|
|
|
|
|
impl ExerciseInfo {
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Path to the exercise file starting with the `exercises/` directory.
|
2024-04-13 20:41:19 -04:00
|
|
|
pub fn path(&self) -> String {
|
2024-05-13 16:02:45 -04:00
|
|
|
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
|
2024-04-13 19:15:43 -04:00
|
|
|
} else {
|
2024-05-13 16:02:45 -04:00
|
|
|
// 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
|
2024-04-13 19:15:43 -04:00
|
|
|
}
|
2024-05-25 12:19:30 -04:00
|
|
|
|
|
|
|
/// 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
|
|
|
|
}
|
2024-04-13 19:15:43 -04:00
|
|
|
}
|
|
|
|
|
2024-06-01 15:48:15 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-13 16:02:45 -04:00
|
|
|
/// The deserialized `info.toml` file.
|
2024-04-13 19:15:43 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
pub struct InfoFile {
|
2024-05-13 16:02:45 -04:00
|
|
|
/// For possible breaking changes in the future for third-party exercises.
|
2024-04-15 19:22:54 -04:00
|
|
|
pub format_version: u8,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Shown to users when starting with the exercises.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub welcome_message: Option<String>,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Shown to users after finishing all exercises.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub final_message: Option<String>,
|
2024-05-13 16:02:45 -04:00
|
|
|
/// List of all exercises.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub exercises: Vec<ExerciseInfo>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InfoFile {
|
2024-05-13 16:02:45 -04:00
|
|
|
/// Official exercises: Parse the embedded `info.toml` file.
|
|
|
|
/// Third-party exercises: Parse the `info.toml` file in the current directory.
|
2024-04-13 19:15:43 -04:00
|
|
|
pub fn parse() -> Result<Self> {
|
|
|
|
// Read a local `info.toml` if it exists.
|
2024-04-23 18:58:52 -04:00
|
|
|
let slf = match fs::read_to_string("info.toml") {
|
|
|
|
Ok(file_content) => toml_edit::de::from_str::<Self>(&file_content)
|
2024-04-13 19:15:43 -04:00
|
|
|
.context("Failed to parse the `info.toml` file")?,
|
2024-04-23 18:58:52 -04:00
|
|
|
Err(e) => {
|
|
|
|
if e.kind() == ErrorKind::NotFound {
|
2024-05-12 19:25:38 -04:00
|
|
|
return toml_edit::de::from_str(EMBEDDED_FILES.info_file)
|
2024-04-24 10:26:48 -04:00
|
|
|
.context("Failed to parse the embedded `info.toml` file");
|
2024-04-13 19:15:43 -04:00
|
|
|
}
|
2024-04-23 18:58:52 -04:00
|
|
|
|
|
|
|
return Err(Error::from(e).context("Failed to read the `info.toml` file"));
|
|
|
|
}
|
2024-04-13 19:15:43 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
if slf.exercises.is_empty() {
|
|
|
|
bail!("{NO_EXERCISES_ERR}");
|
|
|
|
}
|
|
|
|
|
|
|
|
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.";
|