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