summaryrefslogtreecommitdiff
path: root/src/info_file.rs
blob: 04e5d644a2da73fc9ff6d609846203e2b9bb140e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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: String,
    /// Exercise's directory name inside the `exercises/` directory.
    pub dir: Option<String>,
    /// 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: String,
    /// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
    #[serde(default)]
    pub skip_check_unsolved: bool,
}
#[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 {
        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.as_deref()
    }

    #[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<String>,
    /// Shown to users after finishing all exercises.
    pub final_message: Option<String>,
    /// List of all exercises.
    pub exercises: Vec<ExerciseInfo>,
}

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<Self> {
        // Read a local `info.toml` if it exists.
        let slf = match fs::read_to_string("info.toml") {
            Ok(file_content) => toml::de::from_str::<Self>(&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.";