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 #[inline]
258 #[must_use]
259 pub fn teaser(&self) -> Option<&str> {
260 match self {
261 Self::Library { teaser, .. }
262 | Self::Book { teaser, .. }
263 | Self::Paper { teaser, .. }
264 | Self::Course { teaser, .. }
265 | Self::SelfStudy { teaser, .. } => teaser.as_deref(),
266 }
267 }
268 pub fn set_teaser(&mut self, new_teaser: Box<str>) {
269 match self {
270 Self::Library { teaser, .. }
271 | Self::Book { teaser, .. }
272 | Self::Paper { teaser, .. }
273 | Self::Course { teaser, .. }
274 | Self::SelfStudy { teaser, .. } => *teaser = Some(new_teaser),
275 }
276 }
277}
278impl Eq for ArchiveIndex {}
279impl PartialEq for ArchiveIndex {
280 fn eq(&self, other: &Self) -> bool {
281 match (self, other) {
282 (Self::Library { archive: a1, .. }, Self::Library { archive: a2, .. }) => a1 == a2,
283 (Self::Book { file: f1, .. }, Self::Book { file: f2, .. })
284 | (Self::Course { notes: f1, .. }, Self::Course { notes: f2, .. })
285 | (Self::Paper { file: f1, .. }, Self::Paper { file: f2, .. })
286 | (Self::SelfStudy { notes: f1, .. }, Self::SelfStudy { notes: f2, .. }) => f1 == f2,
287 _ => false,
288 }
289 }
290}
291
292#[derive(Clone, Debug)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294#[cfg_attr(feature = "typescript", derive(tsify::Tsify))]
295#[cfg_attr(feature = "typescript", tsify(into_wasm_abi, from_wasm_abi))]
296pub struct Instance {
297 pub semester: Box<str>,
298 #[cfg_attr(feature = "serde", serde(default))]
299 pub instructors: Option<Box<[Box<str>]>>,
300 #[cfg_attr(feature = "serde", serde(rename = "TAs"))]
301 #[cfg_attr(feature = "serde", serde(default))]
302 pub tas: Option<Box<[Box<str>]>>,
303 #[cfg_attr(feature = "serde", serde(rename = "leadTAs"))]
304 #[cfg_attr(feature = "serde", serde(default))]
305 pub lead_tas: Option<Box<[Box<str>]>>,
306}
307
308#[derive(Debug, thiserror::Error)]
309pub enum IndexParseError {
310 #[error("invalid uri: {0}")]
311 Uri(#[from] UriParseError),
312}
313
314impl ArchiveIndex {
315 #[allow(clippy::too_many_lines)]
317 pub fn from_kind(
318 d: DocumentKind,
319 a: &ArchiveUri,
320 images: impl FnMut(Box<str>) -> Box<str>,
321 ) -> Result<Self, IndexParseError> {
322 Ok(match d {
323 DocumentKind::Library {
324 title,
325 teaser,
326 thumbnail,
327 } => Self::Library {
328 archive: a.archive_id().clone(),
329 title,
330 teaser,
331 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
332 None
333 } else {
334 thumbnail.map(images)
335 },
336 },
337 DocumentKind::Book {
338 title,
339 authors,
340 file,
341 teaser,
342 thumbnail,
343 } => Self::Book {
344 title,
345 teaser,
346 file: DocumentUri::from_archive_relpath(a.clone(), &file)?,
347 authors: authors.into_iter().map(|is| is.name).collect(),
348 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
349 None
350 } else {
351 thumbnail.map(images)
352 },
353 },
354 DocumentKind::Paper {
355 title,
356 authors,
357 file,
358 teaser,
359 thumbnail,
360 venue,
361 venue_url,
362 } => Self::Paper {
363 title,
364 teaser,
365 venue,
366 venue_url,
367 file: DocumentUri::from_archive_relpath(a.clone(), &file)?,
368 authors: authors.into_iter().map(|is| is.name).collect(),
369 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
370 None
371 } else {
372 thumbnail.map(images)
373 },
374 },
375 DocumentKind::Course {
376 title,
377 landing,
378 acronym,
379 authors: instructors,
380 institution,
381 notes,
382 slides,
383 thumbnail,
384 teaser,
388 } => Self::Course {
389 title,
390 acronym,
391 institution,
392 teaser,
395 landing: DocumentUri::from_archive_relpath(a.clone(), &landing)?,
396 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
397 None
398 } else {
399 thumbnail.map(images)
400 },
401 notes: DocumentUri::from_archive_relpath(a.clone(), ¬es)?,
402 slides: if slides.as_ref().is_some_and(|s| s.is_empty()) {
403 None
404 } else {
405 slides
406 .map(|s| DocumentUri::from_archive_relpath(a.clone(), &s))
407 .transpose()?
408 },
409 authors: instructors.into_iter().map(|is| is.name).collect(),
423 },
424 DocumentKind::SelfStudy {
425 title,
426 landing,
427 acronym,
428 notes,
429 slides,
430 thumbnail,
431 teaser,
432 authors,
433 } => Self::SelfStudy {
434 title,
435 acronym,
436 teaser,
437 landing: DocumentUri::from_archive_relpath(a.clone(), &landing)?,
438 thumbnail: if thumbnail.as_ref().is_some_and(|s| s.is_empty()) {
439 None
440 } else {
441 thumbnail.map(images)
442 },
443 notes: DocumentUri::from_archive_relpath(a.clone(), ¬es)?,
444 slides: if slides.as_ref().is_some_and(|s| s.is_empty()) {
445 None
446 } else {
447 slides
448 .map(|s| DocumentUri::from_archive_relpath(a.clone(), &s))
449 .transpose()?
450 },
451 authors: authors.into_iter().map(|is| is.name).collect(),
452 },
453 })
454 }
455}