1use crate::{
2 file_states::FileStateSummary,
3 uris::{ArchiveId, ArchiveURI, ArchiveURITrait, DocumentURI},
4};
5
6#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
7#[serde(untagged)]
8pub enum ArchiveDatum {
9 Document(DocumentKind),
10 Institution(Institution),
11}
12
13#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
14#[serde(tag = "type")]
15pub enum DocumentKind {
16 #[serde(rename = "library")]
17 Library {
18 title: Box<str>,
19 teaser: Option<Box<str>>,
20 thumbnail: Option<Box<str>>,
21 },
22 #[serde(rename = "book")]
23 Book {
24 title: Box<str>,
25 authors: Vec<Person>,
26 file: Box<str>,
27 thumbnail: Option<Box<str>>,
28 teaser: Option<Box<str>>,
29 },
30 #[serde(rename = "paper")]
31 Paper {
32 title: Box<str>,
33 authors: Vec<Person>,
34 file: Box<str>,
35 thumbnail: Option<Box<str>>,
36 teaser: Option<Box<str>>,
37 venue: Option<Box<str>>,
38 venue_url: Option<Box<str>>,
39 },
40 #[serde(rename = "course")]
41 Course {
42 title: Box<str>,
43 landing: Box<str>,
44 acronym: Option<Box<str>>,
45 instructors: Vec<Person>,
46 institution: Box<str>,
47 notes: Box<str>,
48 slides: Option<Box<str>>,
49 thumbnail: Option<Box<str>>,
50 #[serde(default)]
51 quizzes: bool,
52 #[serde(default)]
53 homeworks: bool,
54 #[serde(default)]
55 instances: Vec<PreInstance>,
56 teaser: Option<Box<str>>,
57 },
58 #[serde(rename = "self-study")]
59 SelfStudy {
60 title: Box<str>,
61 landing: Box<str>,
62 acronym: Option<Box<str>>,
63 notes: Box<str>,
64 slides: Option<Box<str>>,
65 teaser: Option<Box<str>>,
66 thumbnail: Option<Box<str>>,
67 },
68}
69impl DocumentKind {
70 #[inline]
71 #[must_use]
72 pub fn teaser(&self) -> Option<&str> {
73 match self {
74 Self::Library { teaser, .. }
75 | Self::Book { teaser, .. }
76 | Self::Paper { teaser, .. }
77 | Self::Course { teaser, .. }
78 | Self::SelfStudy { teaser, .. } => teaser.as_deref(),
79 }
80 }
81 pub fn set_teaser(&mut self, new_teaser: Box<str>) {
82 match self {
83 Self::Library { teaser, .. }
84 | Self::Book { teaser, .. }
85 | Self::Paper { teaser, .. }
86 | Self::Course { teaser, .. }
87 | Self::SelfStudy { teaser, .. } => *teaser = Some(new_teaser),
88 }
89 }
90}
91
92#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
93#[serde(tag = "type")]
94#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
95#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
96pub enum Institution {
97 #[serde(rename = "university")]
98 University {
99 title: Box<str>,
100 place: Box<str>,
101 country: Box<str>,
102 url: Box<str>,
103 acronym: Box<str>,
104 logo: Box<str>,
105 },
106 #[serde(rename = "school")]
107 School {
108 title: Box<str>,
109 place: Box<str>,
110 country: Box<str>,
111 url: Box<str>,
112 acronym: Box<str>,
113 logo: Box<str>,
114 },
115}
116impl Institution {
117 #[inline]
118 #[must_use]
119 pub const fn acronym(&self) -> &str {
120 match self {
121 Self::University { acronym, .. } | Self::School { acronym, .. } => acronym,
122 }
123 }
124 #[inline]
125 #[must_use]
126 pub const fn url(&self) -> &str {
127 match self {
128 Self::University { url, .. } | Self::School { url, .. } => url,
129 }
130 }
131 #[inline]
132 #[must_use]
133 pub const fn title(&self) -> &str {
134 match self {
135 Self::University { title, .. } | Self::School { title, .. } => title,
136 }
137 }
138 #[inline]
139 #[must_use]
140 pub const fn logo(&self) -> &str {
141 match self {
142 Self::University { logo, .. } | Self::School { logo, .. } => logo,
143 }
144 }
145}
146impl PartialEq for Institution {
147 fn eq(&self, other: &Self) -> bool {
148 match (self, other) {
149 (Self::University { title: t1, .. }, Self::University { title: t2, .. })
150 | (Self::School { title: t1, .. }, Self::School { title: t2, .. }) => t1 == t2,
151 _ => false,
152 }
153 }
154}
155
156#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
157pub struct Person {
158 pub name: Box<str>,
159}
160
161#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
162pub struct PreInstance {
163 pub semester: Box<str>,
164 pub instructors: Option<Vec<Person>>,
165 #[serde(rename = "TAs")]
166 pub tas: Option<Vec<Person>>,
167 #[serde(rename = "leadTAs")]
168 pub lead_tas: Option<Vec<Person>>,
169}
170
171#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
172#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
173#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
174#[serde(tag = "type")]
175pub enum ArchiveIndex {
176 #[serde(rename = "library")]
177 Library {
178 archive: ArchiveId,
179 title: Box<str>,
180 #[serde(default)]
181 teaser: Option<Box<str>>,
182 #[serde(default)]
183 thumbnail: Option<Box<str>>,
184 },
185 #[serde(rename = "book")]
186 Book {
187 title: Box<str>,
188 authors: Box<[Box<str>]>,
189 file: DocumentURI,
190 #[serde(default)]
191 teaser: Option<Box<str>>,
192 #[serde(default)]
193 thumbnail: Option<Box<str>>,
194 },
195 #[serde(rename = "paper")]
196 Paper {
197 title: Box<str>,
198 authors: Box<[Box<str>]>,
199 file: DocumentURI,
200 #[serde(default)]
201 thumbnail: Option<Box<str>>,
202 #[serde(default)]
203 teaser: Option<Box<str>>,
204 #[serde(default)]
205 venue: Option<Box<str>>,
206 #[serde(default)]
207 venue_url: Option<Box<str>>,
208 },
209 #[serde(rename = "course")]
210 Course {
211 title: Box<str>,
212 landing: DocumentURI,
213 acronym: Option<Box<str>>,
214 instructors: Box<[Box<str>]>,
215 institution: Box<str>,
216 instances: Box<[Instance]>,
217 notes: DocumentURI,
218 #[serde(default)]
219 slides: Option<DocumentURI>,
220 #[serde(default)]
221 thumbnail: Option<Box<str>>,
222 #[serde(default)]
223 quizzes: bool,
224 #[serde(default)]
225 homeworks: bool,
226 #[serde(default)]
227 teaser: Option<Box<str>>,
228 },
229 #[serde(rename = "self-study")]
230 SelfStudy {
231 title: Box<str>,
232 landing: DocumentURI,
233 notes: DocumentURI,
234 #[serde(default)]
235 acronym: Option<Box<str>>,
236 #[serde(default)]
237 slides: Option<DocumentURI>,
238 #[serde(default)]
239 thumbnail: Option<Box<str>>,
240 #[serde(default)]
241 teaser: Option<Box<str>>,
242 },
243}
244impl ArchiveIndex {
245 #[inline]
246 #[must_use]
247 pub fn teaser(&self) -> Option<&str> {
248 match self {
249 Self::Library { teaser, .. }
250 | Self::Book { teaser, .. }
251 | Self::Paper { teaser, .. }
252 | Self::Course { teaser, .. }
253 | Self::SelfStudy { teaser, .. } => teaser.as_deref(),
254 }
255 }
256 pub fn set_teaser(&mut self, new_teaser: Box<str>) {
257 match self {
258 Self::Library { teaser, .. }
259 | Self::Book { teaser, .. }
260 | Self::Paper { teaser, .. }
261 | Self::Course { teaser, .. }
262 | Self::SelfStudy { teaser, .. } => *teaser = Some(new_teaser),
263 }
264 }
265}
266impl Eq for ArchiveIndex {}
267impl PartialEq for ArchiveIndex {
268 fn eq(&self, other: &Self) -> bool {
269 match (self, other) {
270 (Self::Library { archive: a1, .. }, Self::Library { archive: a2, .. }) => a1 == a2,
271 (Self::Book { file: f1, .. }, Self::Book { file: f2, .. })
272 | (Self::Course { notes: f1, .. }, Self::Course { notes: f2, .. })
273 | (Self::Paper { file: f1, .. }, Self::Paper { file: f2, .. })
274 | (Self::SelfStudy { notes: f1, .. }, Self::SelfStudy { notes: f2, .. }) => f1 == f2,
275 _ => false,
276 }
277 }
278}
279impl ArchiveIndex {
280 pub fn from_kind(
281 d: DocumentKind,
282 a: &ArchiveURI,
283 images: impl FnMut(Box<str>) -> Box<str>,
284 ) -> eyre::Result<Self> {
285 Ok(match d {
286 DocumentKind::Library {
287 title,
288 teaser,
289 thumbnail,
290 } => Self::Library {
291 archive: a.archive_id().clone(),
292 title,
293 teaser,
294 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
295 None
296 } else {
297 thumbnail.map(images)
298 },
299 },
300 DocumentKind::Book {
301 title,
302 authors,
303 file,
304 teaser,
305 thumbnail,
306 } => Self::Book {
307 title,
308 teaser,
309 file: DocumentURI::from_archive_relpath(a.clone(), &file)?,
310 authors: authors.into_iter().map(|is| is.name).collect(),
311 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
312 None
313 } else {
314 thumbnail.map(images)
315 },
316 },
317 DocumentKind::Paper {
318 title,
319 authors,
320 file,
321 teaser,
322 thumbnail,
323 venue,
324 venue_url,
325 } => Self::Paper {
326 title,
327 teaser,
328 venue,
329 venue_url,
330 file: DocumentURI::from_archive_relpath(a.clone(), &file)?,
331 authors: authors.into_iter().map(|is| is.name).collect(),
332 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
333 None
334 } else {
335 thumbnail.map(images)
336 },
337 },
338 DocumentKind::Course {
339 title,
340 landing,
341 acronym,
342 instructors,
343 institution,
344 notes,
345 slides,
346 thumbnail,
347 quizzes,
348 homeworks,
349 instances,
350 teaser,
351 } => Self::Course {
352 title,
353 acronym,
354 institution,
355 quizzes,
356 homeworks,
357 teaser,
358 landing: DocumentURI::from_archive_relpath(a.clone(), &landing)?,
359 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
360 None
361 } else {
362 thumbnail.map(images)
363 },
364 notes: DocumentURI::from_archive_relpath(a.clone(), ¬es)?,
365 slides: if slides.as_ref().is_some_and(|s| s.is_empty()) {
366 None
367 } else {
368 slides
369 .map(|s| DocumentURI::from_archive_relpath(a.clone(), &s))
370 .transpose()?
371 },
372 instances: instances
373 .into_iter()
374 .map(|i| Instance {
375 semester: i.semester,
376 instructors: i
377 .instructors
378 .map(|is| is.into_iter().map(|i| i.name).collect()),
379 tas: i.tas.map(|is| is.into_iter().map(|i| i.name).collect()),
380 lead_tas: i
381 .lead_tas
382 .map(|is| is.into_iter().map(|i| i.name).collect()),
383 })
384 .collect(),
385 instructors: instructors.into_iter().map(|is| is.name).collect(),
386 },
387 DocumentKind::SelfStudy {
388 title,
389 landing,
390 acronym,
391 notes,
392 slides,
393 thumbnail,
394 teaser,
395 } => Self::SelfStudy {
396 title,
397 acronym,
398 teaser,
399 landing: DocumentURI::from_archive_relpath(a.clone(), &landing)?,
400 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
401 None
402 } else {
403 thumbnail.map(images)
404 },
405 notes: DocumentURI::from_archive_relpath(a.clone(), ¬es)?,
406 slides: if slides.as_ref().is_some_and(|s| s.is_empty()) {
407 None
408 } else {
409 slides
410 .map(|s| DocumentURI::from_archive_relpath(a.clone(), &s))
411 .transpose()?
412 },
413 },
414 })
415 }
416}
417
418#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
419#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
420#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
421pub struct Instance {
422 pub semester: Box<str>,
423 #[serde(default)]
424 pub instructors: Option<Box<[Box<str>]>>,
425 #[serde(rename = "TAs")]
426 #[serde(default)]
427 pub tas: Option<Box<[Box<str>]>>,
428 #[serde(rename = "leadTAs")]
429 #[serde(default)]
430 pub lead_tas: Option<Box<[Box<str>]>>,
431}
432
433#[cfg(unix)]
434#[test]
435fn test() {
436 use std::os::unix::ffi::OsStrExt;
437 tracing_subscriber::fmt().init();
438 let mathhubs: Vec<_> = std::env::var("MATHHUB")
439 .expect("No MathHub directory")
440 .split(',')
441 .map(|s| std::path::PathBuf::from(s.trim()))
442 .collect();
443
444 for m in mathhubs {
445 for entry in walkdir::WalkDir::new(m) {
446 let entry = entry.expect("Error reading directory");
447 if entry.file_type().is_file()
448 && entry
449 .path()
450 .extension()
451 .is_some_and(|s| s.as_bytes() == b"json")
452 && entry
453 .path()
454 .file_stem()
455 .is_some_and(|s| s.as_bytes() == b"archive")
456 {
457 tracing::info!("File: {}", entry.path().display());
458 let data = std::fs::read_to_string(entry.path()).expect("Error reading file");
459 let data: Vec<ArchiveDatum> =
460 serde_json::from_str(&data).expect("Error parsing JSON");
461 for d in data {
462 tracing::info!("{d:#?}");
463 }
464 }
465 }
466 }
467}
468
469#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
470#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
471#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
472pub struct ArchiveData {
473 pub id: ArchiveId,
474 #[serde(default)]
475 pub git: Option<String>,
476 #[serde(default)]
477 pub summary: Option<FileStateSummary>,
478}
479
480#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
481#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
482#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
483pub struct ArchiveGroupData {
484 pub id: ArchiveId,
485 #[serde(default)]
486 pub summary: Option<FileStateSummary>,
487}
488
489#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
490#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
491#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
492pub struct DirectoryData {
493 pub rel_path: String,
494 #[serde(default)]
495 pub summary: Option<FileStateSummary>,
496}
497
498#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
499#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
500#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
501pub struct FileData {
502 pub rel_path: String,
503 pub format: String, }