tmc_langs/
data.rs

1//! Various data types.
2
3use crate::error::{LangsError, ParamError};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::{
7    collections::HashMap,
8    fmt::{Display, Formatter, Result as FmtResult},
9    path::PathBuf,
10};
11use tmc_testmycode_client::response::{CourseData, CourseDetails, CourseExercise};
12use uuid::Uuid;
13
14#[derive(Debug, Serialize, Deserialize, JsonSchema)]
15#[serde(rename_all = "snake_case")]
16#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
17pub enum LocalExercise {
18    Tmc(LocalTmcExercise),
19    Mooc(LocalMoocExercise),
20}
21
22/// TMC eercise inside the projects directory.
23#[derive(Debug, Serialize, Deserialize, JsonSchema)]
24#[serde(rename_all = "kebab-case")]
25#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
26pub struct LocalTmcExercise {
27    pub exercise_slug: String,
28    pub exercise_path: PathBuf,
29}
30
31/// MOOC exercise inside the projects directory.
32#[derive(Debug, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "kebab-case")]
34#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
35pub struct LocalMoocExercise {
36    pub exercise_id: Uuid,
37    pub exercise_path: PathBuf,
38}
39
40/// TmcParams is used to safely construct data for a .tmcparams file, which contains lines in the form of
41/// export A=B
42/// export C=(D, E, F)
43/// the keys and values of the inner hashmap are validated to make sure they are valid as bash variables
44#[derive(Debug, Default)]
45pub struct TmcParams(pub HashMap<ShellString, TmcParam>);
46
47impl TmcParams {
48    pub fn new() -> Self {
49        Self(HashMap::new())
50    }
51
52    pub fn insert_string<S: AsRef<str>, T: AsRef<str>>(
53        &mut self,
54        key: S,
55        value: T,
56    ) -> Result<(), LangsError> {
57        // validate key
58        let key = {
59            let key = key.as_ref();
60            match Self::is_valid_key(key) {
61                Ok(()) => ShellString(key.to_string()),
62                Err(e) => return Err(LangsError::InvalidParam(key.to_string(), e)),
63            }
64        };
65
66        // validate value
67        let value = {
68            let value = value.as_ref();
69            match Self::is_valid_value(value) {
70                Ok(()) => ShellString(value.to_string()),
71                Err(e) => return Err(LangsError::InvalidParam(value.to_string(), e)),
72            }
73        };
74
75        self.0.insert(key, TmcParam::String(value));
76        Ok(())
77    }
78
79    pub fn insert_array<S: AsRef<str>, T: AsRef<str>>(
80        &mut self,
81        key: S,
82        values: Vec<T>,
83    ) -> Result<(), LangsError> {
84        let key = {
85            let key = key.as_ref();
86            match Self::is_valid_key(key) {
87                Ok(()) => ShellString(key.to_string()),
88                Err(e) => return Err(LangsError::InvalidParam(key.to_string(), e)),
89            }
90        };
91
92        let values = values
93            .into_iter()
94            .map(|s| {
95                let s = s.as_ref();
96                match Self::is_valid_value(s) {
97                    Ok(()) => Ok(ShellString(s.to_string())),
98                    Err(e) => Err(LangsError::InvalidParam(s.to_string(), e)),
99                }
100            })
101            .collect::<Result<Vec<_>, _>>()?;
102
103        self.0.insert(key, TmcParam::Array(values));
104        Ok(())
105    }
106
107    fn is_valid_key<S: AsRef<str>>(string: S) -> Result<(), ParamError> {
108        if string.as_ref().is_empty() {
109            return Err(ParamError::Empty);
110        }
111
112        for c in string.as_ref().chars() {
113            if !c.is_ascii_alphabetic() && c != '_' {
114                return Err(ParamError::InvalidChar(c));
115            }
116        }
117        Ok(())
118    }
119
120    fn is_valid_value<S: AsRef<str>>(string: S) -> Result<(), ParamError> {
121        if string.as_ref().is_empty() {
122            return Err(ParamError::Empty);
123        }
124
125        for c in string.as_ref().chars() {
126            if !c.is_ascii_alphabetic() && c != '_' && c != '-' {
127                return Err(ParamError::InvalidChar(c));
128            }
129        }
130        Ok(())
131    }
132}
133
134// string checked to be a valid shell string
135#[derive(Debug, PartialEq, Eq, Hash)]
136pub struct ShellString(String);
137
138/// .tmcparams variables can be strings or arrays
139#[derive(Debug)]
140pub enum TmcParam {
141    String(ShellString),
142    Array(Vec<ShellString>),
143}
144
145// the Display impl escapes the inner strings with shellwords
146impl Display for TmcParam {
147    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
148        match self {
149            Self::String(s) => s.fmt(f),
150            Self::Array(v) => write!(
151                f,
152                "( {} )",
153                v.iter()
154                    .map(|s| s.to_string())
155                    .collect::<Vec<_>>()
156                    .join(" ")
157            ),
158        }
159    }
160}
161
162// the Display impl escapes the inner string with shellwords
163impl Display for ShellString {
164    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
165        write!(f, "{}", shellwords::escape(&self.0))
166    }
167}
168
169#[derive(Debug)]
170pub enum DownloadResult {
171    Success {
172        downloaded: Vec<TmcExerciseDownload>,
173        skipped: Vec<TmcExerciseDownload>,
174    },
175    Failure {
176        downloaded: Vec<TmcExerciseDownload>,
177        skipped: Vec<TmcExerciseDownload>,
178        failed: Vec<(TmcExerciseDownload, Vec<String>)>,
179    },
180}
181
182pub struct DownloadTarget {
183    pub target: TmcExerciseDownload,
184    pub checksum: String,
185    pub kind: DownloadTargetKind,
186}
187
188pub enum DownloadTargetKind {
189    Template,
190    Submission { submission_id: u32 },
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
194#[serde(rename_all = "kebab-case")]
195#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
196pub struct TmcExerciseDownload {
197    pub id: u32,
198    pub course_slug: String,
199    pub exercise_slug: String,
200    pub path: PathBuf,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
204#[serde(rename_all = "kebab-case")]
205#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
206pub struct MoocExerciseDownload {
207    pub id: Uuid,
208    pub path: PathBuf,
209}
210
211#[derive(Debug, Serialize, Deserialize, JsonSchema)]
212#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
213pub struct CombinedCourseData {
214    pub details: CourseDetails,
215    pub exercises: Vec<CourseExercise>,
216    pub settings: CourseData,
217}
218
219#[derive(Debug, Serialize, Deserialize, JsonSchema)]
220#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
221pub struct DownloadOrUpdateTmcCourseExercisesResult {
222    pub downloaded: Vec<TmcExerciseDownload>,
223    pub skipped: Vec<TmcExerciseDownload>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub failed: Option<Vec<(TmcExerciseDownload, Vec<String>)>>,
226}
227
228/// A setting in a TmcConfig file.
229#[derive(Debug, Serialize, Deserialize, Clone)]
230#[serde(untagged)]
231#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
232pub enum ConfigValue {
233    Value(Option<toml::Value>),
234    Path(PathBuf),
235}
236
237#[derive(Debug, Serialize, Deserialize, JsonSchema)]
238#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
239pub struct DownloadOrUpdateMoocCourseExercisesResult {
240    pub downloaded: Vec<MoocExerciseDownload>,
241    pub skipped: Vec<MoocExerciseDownload>,
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub failed: Option<Vec<(MoocExerciseDownload, Vec<String>)>>,
244}