1use serde::{Deserialize, Serialize};
4use utoipa::ToSchema;
5
6use crate::prelude::*;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
10#[serde(tag = "schema", content = "payload", rename_all = "snake_case")]
11pub enum CourseDesignerStageWorkspace {
12 AnalysisV1(AnalysisWorkspaceV1),
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum AnalysisCourseType {
18 Compulsory,
19 Elective,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
24#[serde(rename_all = "snake_case")]
25#[derive(Default)]
26pub struct AnalysisWorkspaceV1 {
27 pub course_title: Option<String>,
28 pub credits: Option<f64>,
29 pub language: Option<String>,
30 pub target_group: Option<String>,
31 pub mode_synchronous: bool,
32 pub mode_asynchronous: bool,
33 pub open_period_i: bool,
34 pub open_period_ii: bool,
35 pub open_period_iii: bool,
36 pub open_period_iv: bool,
37 pub open_period_all: bool,
38 pub responsible_teachers: Option<String>,
39 pub degree_programme: Option<String>,
40 pub course_type: Option<AnalysisCourseType>,
41 pub students_demographic_data: Option<String>,
42 pub wishes_topics: Option<String>,
43 pub wishes_content_format_text: bool,
44 pub wishes_content_format_video: bool,
45 pub wishes_content_format_podcast: bool,
46 pub wishes_content_format_xr: bool,
47 pub wishes_content_format_notes: Option<String>,
48 pub wishes_assessment_text: Option<String>,
49 pub wishes_other_suggestions: Option<String>,
50 pub market_results: Option<String>,
51 pub resources_university: Option<String>,
52 pub resources_purchase_budget: Option<String>,
53 pub contributors_instructional_designer: Option<String>,
54 pub contributors_subject_matter_experts: Option<String>,
55 pub contributors_editors: Option<String>,
56 pub contributors_support_staff: Option<String>,
57}
58
59impl AnalysisWorkspaceV1 {
60 pub fn validate(&self) -> ModelResult<()> {
62 if let Some(c) = self.credits
63 && (!c.is_finite() || c < 0.0)
64 {
65 return Err(ModelError::new(
66 ModelErrorType::InvalidRequest,
67 "Credits must be a non-negative finite number.".to_string(),
68 None,
69 ));
70 }
71 Ok(())
72 }
73}
74
75pub fn parse_workspace_data(
77 value: Option<serde_json::Value>,
78) -> ModelResult<Option<CourseDesignerStageWorkspace>> {
79 match value {
80 None => Ok(None),
81 Some(v) if v.is_null() => Ok(None),
82 Some(v) => {
83 let parsed: CourseDesignerStageWorkspace = serde_json::from_value(v).map_err(|e| {
84 ModelError::new(
85 ModelErrorType::InvalidRequest,
86 format!("Invalid workspace_data: {e}"),
87 None,
88 )
89 })?;
90 match &parsed {
91 CourseDesignerStageWorkspace::AnalysisV1(a) => a.validate()?,
92 }
93 Ok(Some(parsed))
94 }
95 }
96}
97
98pub fn workspace_to_json(
100 workspace: Option<CourseDesignerStageWorkspace>,
101) -> ModelResult<Option<serde_json::Value>> {
102 match workspace {
103 None => Ok(None),
104 Some(w) => {
105 match &w {
106 CourseDesignerStageWorkspace::AnalysisV1(a) => a.validate()?,
107 }
108 serde_json::to_value(w).map(Some).map_err(|e| {
109 ModelError::new(
110 ModelErrorType::Json,
111 format!("Failed to serialize workspace: {e}"),
112 None,
113 )
114 })
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn analysis_workspace_round_trip() {
125 let w = AnalysisWorkspaceV1 {
126 course_title: Some("Test".to_string()),
127 credits: Some(5.5),
128 language: Some("en".to_string()),
129 target_group: None,
130 mode_synchronous: true,
131 mode_asynchronous: false,
132 open_period_i: true,
133 open_period_ii: false,
134 open_period_iii: false,
135 open_period_iv: false,
136 open_period_all: false,
137 responsible_teachers: Some("A, B".to_string()),
138 degree_programme: None,
139 course_type: Some(AnalysisCourseType::Elective),
140 students_demographic_data: None,
141 wishes_topics: None,
142 wishes_content_format_text: true,
143 wishes_content_format_video: false,
144 wishes_content_format_podcast: false,
145 wishes_content_format_xr: false,
146 wishes_content_format_notes: None,
147 wishes_assessment_text: None,
148 wishes_other_suggestions: None,
149 market_results: None,
150 resources_university: None,
151 resources_purchase_budget: None,
152 contributors_instructional_designer: None,
153 contributors_subject_matter_experts: None,
154 contributors_editors: None,
155 contributors_support_staff: None,
156 };
157 let env = CourseDesignerStageWorkspace::AnalysisV1(w.clone());
158 let v = serde_json::to_value(&env).unwrap();
159 assert!(v.get("payload").is_some());
160 let back: CourseDesignerStageWorkspace = serde_json::from_value(v).unwrap();
161 assert!(matches!(
162 back,
163 CourseDesignerStageWorkspace::AnalysisV1(ref a) if a.course_title.as_deref() == Some("Test")
164 ));
165 }
166}