tmc_mooc_client/
exercise.rs

1use chrono::{DateTime, Utc};
2use mooc_langs_api as api;
3use serde::{Deserialize, Serialize};
4use tmc_langs_util::{JsonError, deserialize};
5#[cfg(feature = "ts-rs")]
6use ts_rs::TS;
7use uuid::Uuid;
8
9#[derive(Debug, Serialize, Deserialize)]
10#[cfg_attr(feature = "ts-rs", derive(TS))]
11pub struct TmcExerciseSlide {
12    pub slide_id: Uuid,
13    pub exercise_id: Uuid,
14    pub exercise_name: String,
15    pub exercise_order_number: i32,
16    pub deadline: Option<DateTime<Utc>>,
17    pub tasks: Vec<TmcExerciseTask>,
18}
19
20impl TryFrom<api::ExerciseSlide> for TmcExerciseSlide {
21    type Error = JsonError;
22    fn try_from(value: api::ExerciseSlide) -> Result<Self, Self::Error> {
23        let slide = Self {
24            slide_id: value.slide_id,
25            exercise_id: value.exercise_id,
26            exercise_name: value.exercise_name,
27            exercise_order_number: value.exercise_order_number,
28            deadline: value.deadline,
29            tasks: value
30                .tasks
31                .into_iter()
32                .map(TryFrom::try_from)
33                .collect::<Result<_, _>>()?,
34        };
35        Ok(slide)
36    }
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40#[cfg_attr(feature = "ts-rs", derive(TS))]
41pub struct TmcExerciseTask {
42    pub task_id: Uuid,
43    pub order_number: i32,
44    pub assignment: serde_json::Value,
45    pub public_spec: Option<PublicSpec>,
46    pub model_solution_spec: Option<ModelSolutionSpec>,
47}
48
49impl TryFrom<api::ExerciseTask> for TmcExerciseTask {
50    type Error = JsonError;
51    fn try_from(value: api::ExerciseTask) -> Result<Self, Self::Error> {
52        let task = Self {
53            task_id: value.task_id,
54            order_number: value.order_number,
55            assignment: value.assignment,
56            public_spec: value
57                .public_spec
58                .map(deserialize::json_from_value)
59                .transpose()?,
60            model_solution_spec: value
61                .model_solution_spec
62                .map(deserialize::json_from_value)
63                .transpose()?,
64        };
65        Ok(task)
66    }
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70#[serde(tag = "type")]
71#[cfg_attr(feature = "ts-rs", derive(TS))]
72pub enum PublicSpec {
73    Browser {
74        files: Vec<ExerciseFile>,
75    },
76    Editor {
77        archive_name: String,
78        archive_download_url: String,
79        checksum: String,
80    },
81}
82
83#[derive(Debug, Serialize, Deserialize)]
84#[serde(tag = "type")]
85pub enum UserAnswer {
86    Browser { files: Vec<ExerciseFile> },
87    Editor { archive_download_url: String },
88}
89
90#[derive(Debug, Serialize, Deserialize)]
91#[serde(tag = "type")]
92#[cfg_attr(feature = "ts-rs", derive(TS))]
93pub enum ModelSolutionSpec {
94    Browser { solution_files: Vec<ExerciseFile> },
95    Editor { download_url: String },
96}
97
98#[derive(Debug, Serialize, Deserialize)]
99#[cfg_attr(feature = "ts-rs", derive(TS))]
100pub struct ExerciseFile {
101    filepath: String,
102    contents: String,
103}
104
105#[cfg(test)]
106mod test {
107    use super::*;
108
109    #[test]
110    fn deserializes_browser_public_spec() {
111        let browser_task = r#"
112{
113    "type": "Browser",
114    "files": [
115        {
116            "filepath": "1",
117            "contents": "2"
118        },
119        {
120            "filepath": "3",
121            "contents": "4"
122        }
123    ]
124}
125"#;
126        serde_json::from_str::<PublicSpec>(browser_task).unwrap();
127    }
128
129    #[test]
130    fn deserializes_editor_public_spec() {
131        let editor_task = r#"
132{
133    "type": "Editor",
134    "archive_name": "1",
135    "archive_download_url": "2",
136    "checksum": "abcd"
137}
138"#;
139        serde_json::from_str::<PublicSpec>(editor_task).unwrap();
140    }
141}