1use 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#[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#[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#[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 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 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#[derive(Debug, PartialEq, Eq, Hash)]
136pub struct ShellString(String);
137
138#[derive(Debug)]
140pub enum TmcParam {
141 String(ShellString),
142 Array(Vec<ShellString>),
143}
144
145impl 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
162impl 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#[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}