1use std::collections::HashMap;
2
3use futures::future::BoxFuture;
4use url::Url;
5
6use crate::{
7 exercise_services::{
8 ExerciseService, get_exercise_service_by_exercise_type, get_exercise_services,
9 },
10 prelude::*,
11};
12
13#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
14pub struct ExerciseServiceInfo {
15 pub exercise_service_id: Uuid,
16 pub created_at: DateTime<Utc>,
17 pub updated_at: DateTime<Utc>,
18 pub user_interface_iframe_path: String,
19 pub grade_endpoint_path: String,
20 pub public_spec_endpoint_path: String,
21 pub model_solution_spec_endpoint_path: String,
22 pub has_custom_view: bool,
24}
25
26#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
27pub struct PathInfo {
28 pub exercise_service_id: Uuid,
29 pub user_interface_iframe_path: String,
30 pub grade_endpoint_path: String,
31 pub public_spec_endpoint_path: String,
32 pub model_solution_spec_endpoint_path: String,
33 pub has_custom_view: bool,
35}
36
37#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
38#[cfg_attr(feature = "ts_rs", derive(TS))]
39pub struct CourseMaterialExerciseServiceInfo {
40 pub exercise_iframe_url: String,
41}
42
43#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
44#[cfg_attr(feature = "ts_rs", derive(TS))]
45pub struct ExerciseServiceInfoApi {
46 pub service_name: String,
47 pub user_interface_iframe_path: String,
48 pub grade_endpoint_path: String,
49 pub public_spec_endpoint_path: String,
50 pub model_solution_spec_endpoint_path: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub has_custom_view: Option<bool>,
53}
54
55pub async fn insert(
56 conn: &mut PgConnection,
57 exercise_service_info: &PathInfo,
58) -> ModelResult<ExerciseServiceInfo> {
59 let res = sqlx::query_as!(
60 ExerciseServiceInfo,
61 "
62INSERT INTO exercise_service_info (
63 exercise_service_id,
64 user_interface_iframe_path,
65 grade_endpoint_path,
66 public_spec_endpoint_path,
67 model_solution_spec_endpoint_path,
68 has_custom_view
69 )
70VALUES ($1, $2, $3, $4, $5, $6)
71RETURNING *
72",
73 exercise_service_info.exercise_service_id,
74 exercise_service_info.user_interface_iframe_path,
75 exercise_service_info.grade_endpoint_path,
76 exercise_service_info.public_spec_endpoint_path,
77 exercise_service_info.model_solution_spec_endpoint_path,
78 exercise_service_info.has_custom_view
79 )
80 .fetch_one(conn)
81 .await?;
82 Ok(res)
83}
84
85pub async fn fetch_and_upsert_service_info(
86 conn: &mut PgConnection,
87 exercise_service: &ExerciseService,
88 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
89) -> ModelResult<ExerciseServiceInfo> {
90 let url = match exercise_service
91 .internal_url
92 .clone()
93 .map(|url| Url::parse(&url))
94 {
95 Some(Ok(url)) => url.to_string(),
96
97 Some(Err(e)) => {
98 warn!(
99 "Internal_url provided for {} is not a valid url. Using public_url instead. Error: {}",
100 exercise_service.name,
101 e.to_string()
102 );
103 exercise_service.public_url.clone()
104 }
105 None => exercise_service.public_url.clone(),
106 };
107 let fetched_info = fetch_service_info(url.parse()?).await?;
108 let res = upsert_service_info(conn, exercise_service.id, &fetched_info).await?;
109 Ok(res)
110}
111
112pub async fn upsert_service_info(
113 conn: &mut PgConnection,
114 exercise_service_id: Uuid,
115 update: &ExerciseServiceInfoApi,
116) -> ModelResult<ExerciseServiceInfo> {
117 let res = sqlx::query_as!(
118 ExerciseServiceInfo,
119 r#"
120INSERT INTO exercise_service_info(
121 exercise_service_id,
122 user_interface_iframe_path,
123 grade_endpoint_path,
124 public_spec_endpoint_path,
125 model_solution_spec_endpoint_path,
126 has_custom_view
127 )
128VALUES ($1, $2, $3, $4, $5, $6)
129ON CONFLICT(exercise_service_id) DO UPDATE
130SET user_interface_iframe_path = $2,
131 grade_endpoint_path = $3,
132 public_spec_endpoint_path = $4,
133 model_solution_spec_endpoint_path = $5,
134 has_custom_view = $6
135RETURNING *
136 "#,
137 exercise_service_id,
138 update.user_interface_iframe_path,
139 update.grade_endpoint_path,
140 update.public_spec_endpoint_path,
141 update.model_solution_spec_endpoint_path,
142 update.has_custom_view.unwrap_or_else(|| false)
143 )
144 .fetch_one(conn)
145 .await?;
146 Ok(res)
147}
148
149pub async fn get_service_info(
150 conn: &mut PgConnection,
151 exercise_service_id: Uuid,
152) -> ModelResult<ExerciseServiceInfo> {
153 let res = sqlx::query_as!(
154 ExerciseServiceInfo,
155 r#"
156SELECT *
157FROM exercise_service_info
158WHERE exercise_service_id = $1
159 "#,
160 exercise_service_id
161 )
162 .fetch_one(conn)
163 .await?;
164 Ok(res)
165}
166
167pub async fn get_service_info_by_exercise_type(
168 conn: &mut PgConnection,
169 exercise_type: &str,
170 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
171) -> ModelResult<ExerciseServiceInfo> {
172 let exercise_service = get_exercise_service_by_exercise_type(conn, exercise_type).await?;
173 let service_info =
174 get_upsert_service_info_by_exercise_service(conn, &exercise_service, fetch_service_info)
175 .await?;
176 Ok(service_info)
177}
178
179pub async fn get_all_exercise_services_by_type(
180 conn: &mut PgConnection,
181) -> ModelResult<HashMap<String, (ExerciseService, ExerciseServiceInfo)>> {
182 let mut exercise_services_by_type = HashMap::new();
183 for exercise_service in get_exercise_services(conn).await? {
184 match get_service_info_by_exercise_service(conn, &exercise_service).await {
185 Ok(Some(info)) => {
186 exercise_services_by_type
187 .insert(exercise_service.slug.clone(), (exercise_service, info));
188 }
189 _ => {
190 tracing::error!(
191 "No corresponding service info found for {} ({})",
192 exercise_service.name,
193 exercise_service.id
194 );
195 }
196 }
197 }
198 Ok(exercise_services_by_type)
199}
200
201pub async fn get_upsert_all_exercise_services_by_type(
202 conn: &mut PgConnection,
203 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
204) -> ModelResult<HashMap<String, (ExerciseService, ExerciseServiceInfo)>> {
205 let mut exercise_services_by_type = HashMap::new();
206 for exercise_service in get_exercise_services(conn).await? {
207 match get_upsert_service_info_by_exercise_service(
208 conn,
209 &exercise_service,
210 &fetch_service_info,
211 )
212 .await
213 {
214 Ok(info) => {
215 exercise_services_by_type
216 .insert(exercise_service.slug.clone(), (exercise_service, info));
217 }
218 _ => {
219 tracing::error!(
220 "No corresponding service info found for {} ({})",
221 exercise_service.name,
222 exercise_service.id
223 );
224 }
225 }
226 }
227 Ok(exercise_services_by_type)
228}
229
230pub async fn get_selected_exercise_services_by_type(
231 conn: &mut PgConnection,
232 slugs: &[String],
233 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
234) -> ModelResult<HashMap<String, (ExerciseService, ExerciseServiceInfo)>> {
235 let selected_services = sqlx::query_as!(
236 ExerciseService,
237 "
238SELECT *
239FROM exercise_services
240WHERE slug = ANY($1);",
241 slugs,
242 )
243 .fetch_all(&mut *conn)
244 .await?;
245 let mut exercise_services_by_type = HashMap::new();
246 for exercise_service in selected_services {
247 let info = get_upsert_service_info_by_exercise_service(
248 conn,
249 &exercise_service,
250 &fetch_service_info,
251 )
252 .await?;
253 exercise_services_by_type.insert(exercise_service.slug.clone(), (exercise_service, info));
254 }
255 Ok(exercise_services_by_type)
256}
257
258pub async fn get_upsert_service_info_by_exercise_service(
259 conn: &mut PgConnection,
260 exercise_service: &ExerciseService,
261 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
262) -> ModelResult<ExerciseServiceInfo> {
263 let res = get_service_info(conn, exercise_service.id).await;
264 let service_info = match res {
265 Ok(exercise_service_info) => exercise_service_info,
266 _ => {
267 warn!(
268 "Could not find service info for {} ({}). This is rare and only should happen when a background worker has not had the opportunity to complete their fetching task yet. Trying the fetching here in this worker so that we can continue.",
269 exercise_service.name, exercise_service.slug
270 );
271
272 fetch_and_upsert_service_info(conn, exercise_service, fetch_service_info).await?
273 }
274 };
275 Ok(service_info)
276}
277
278pub async fn get_service_info_by_exercise_service(
279 conn: &mut PgConnection,
280 exercise_service: &ExerciseService,
281) -> ModelResult<Option<ExerciseServiceInfo>> {
282 let res = get_service_info(conn, exercise_service.id).await;
283 let service_info = match res {
284 Ok(exercise_service_info) => exercise_service_info,
285 _ => {
286 warn!(
287 "Could not find service info for {} ({}). This is rare and only should happen when a background worker has not had the opportunity to complete their fetching task yet.",
288 exercise_service.name, exercise_service.slug
289 );
290 return Ok(None);
291 }
292 };
293 Ok(Some(service_info))
294}
295
296pub async fn get_course_material_service_info_by_exercise_type(
301 conn: &mut PgConnection,
302 exercise_type: &str,
303 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
304) -> ModelResult<Option<CourseMaterialExerciseServiceInfo>> {
305 match get_exercise_service_by_exercise_type(conn, exercise_type).await {
306 Ok(exercise_service) => {
307 let full_service_info = get_upsert_service_info_by_exercise_service(
308 conn,
309 &exercise_service,
310 fetch_service_info,
311 )
312 .await;
313 let service_info_option = match full_service_info {
314 Ok(o) => {
315 let mut url =
319 Url::parse(&exercise_service.public_url).map_err(|original_err| {
320 ModelError::new(
321 ModelErrorType::Generic,
322 original_err.to_string(),
323 Some(original_err.into()),
324 )
325 })?;
326 url.set_path(&o.user_interface_iframe_path);
327 url.set_query(None);
328 url.set_fragment(None);
329
330 Some(CourseMaterialExerciseServiceInfo {
331 exercise_iframe_url: url.to_string(),
332 })
333 }
334 _ => None,
335 };
336
337 Ok(service_info_option)
338 }
339 _ => Ok(None),
340 }
341}