1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Bibliography {
9 #[serde(default)]
11 pub style: CitationStyle,
12
13 pub entries: Vec<BibliographyEntry>,
15}
16
17impl Bibliography {
18 #[must_use]
20 pub fn new(style: CitationStyle) -> Self {
21 Self {
22 style,
23 entries: Vec::new(),
24 }
25 }
26
27 pub fn add_entry(&mut self, entry: BibliographyEntry) {
29 self.entries.push(entry);
30 }
31
32 #[must_use]
34 pub fn get(&self, id: &str) -> Option<&BibliographyEntry> {
35 self.entries.iter().find(|e| e.id == id)
36 }
37
38 #[must_use]
40 pub fn contains(&self, id: &str) -> bool {
41 self.entries.iter().any(|e| e.id == id)
42 }
43
44 #[must_use]
46 pub fn len(&self) -> usize {
47 self.entries.len()
48 }
49
50 #[must_use]
52 pub fn is_empty(&self) -> bool {
53 self.entries.is_empty()
54 }
55}
56
57impl Default for Bibliography {
58 fn default() -> Self {
59 Self::new(CitationStyle::default())
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, strum::Display)]
65#[serde(rename_all = "lowercase")]
66pub enum CitationStyle {
67 #[default]
69 #[strum(serialize = "APA")]
70 Apa,
71 #[strum(serialize = "MLA")]
73 Mla,
74 Chicago,
76 #[strum(serialize = "IEEE")]
78 Ieee,
79 Harvard,
81 Vancouver,
83 #[strum(serialize = "ACM")]
85 Acm,
86 Custom,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct BibliographyEntry {
94 pub id: String,
96
97 pub entry_type: EntryType,
99
100 pub title: String,
102
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub authors: Vec<Author>,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub issued: Option<PartialDate>,
110
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub container_title: Option<String>,
114
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub volume: Option<String>,
118
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub issue: Option<String>,
122
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub page: Option<String>,
126
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub doi: Option<String>,
130
131 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub url: Option<String>,
134
135 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub isbn: Option<String>,
138
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub issn: Option<String>,
142
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub publisher: Option<String>,
146
147 #[serde(default, skip_serializing_if = "Option::is_none")]
149 pub publisher_place: Option<String>,
150
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub edition: Option<String>,
154
155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub editors: Vec<Author>,
158
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub abstract_text: Option<String>,
162
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
165 pub keywords: Vec<String>,
166
167 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub language: Option<String>,
170
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub accessed: Option<PartialDate>,
174
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub note: Option<String>,
178}
179
180impl BibliographyEntry {
181 #[must_use]
183 pub fn new(id: impl Into<String>, entry_type: EntryType, title: impl Into<String>) -> Self {
184 Self {
185 id: id.into(),
186 entry_type,
187 title: title.into(),
188 authors: Vec::new(),
189 issued: None,
190 container_title: None,
191 volume: None,
192 issue: None,
193 page: None,
194 doi: None,
195 url: None,
196 isbn: None,
197 issn: None,
198 publisher: None,
199 publisher_place: None,
200 edition: None,
201 editors: Vec::new(),
202 abstract_text: None,
203 keywords: Vec::new(),
204 language: None,
205 accessed: None,
206 note: None,
207 }
208 }
209
210 #[must_use]
212 pub fn with_author(mut self, author: Author) -> Self {
213 self.authors.push(author);
214 self
215 }
216
217 #[must_use]
219 pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
220 self.authors = authors;
221 self
222 }
223
224 #[must_use]
226 pub fn with_issued(mut self, date: PartialDate) -> Self {
227 self.issued = Some(date);
228 self
229 }
230
231 #[must_use]
233 pub fn with_container(mut self, container: impl Into<String>) -> Self {
234 self.container_title = Some(container.into());
235 self
236 }
237
238 #[must_use]
240 pub fn with_volume_issue(mut self, volume: impl Into<String>, issue: Option<String>) -> Self {
241 self.volume = Some(volume.into());
242 self.issue = issue;
243 self
244 }
245
246 #[must_use]
248 pub fn with_pages(mut self, pages: impl Into<String>) -> Self {
249 self.page = Some(pages.into());
250 self
251 }
252
253 #[must_use]
255 pub fn with_doi(mut self, doi: impl Into<String>) -> Self {
256 self.doi = Some(doi.into());
257 self
258 }
259
260 #[must_use]
262 pub fn with_url(mut self, url: impl Into<String>) -> Self {
263 self.url = Some(url.into());
264 self
265 }
266
267 #[must_use]
269 pub fn with_publisher(mut self, publisher: impl Into<String>, place: Option<String>) -> Self {
270 self.publisher = Some(publisher.into());
271 self.publisher_place = place;
272 self
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
278#[serde(rename_all = "camelCase")]
279#[strum(serialize_all = "lowercase")]
280pub enum EntryType {
281 Article,
283 Book,
285 Chapter,
287 Conference,
289 Thesis,
291 Report,
293 Webpage,
295 Patent,
297 Dataset,
299 Software,
301 #[strum(serialize = "legal-case")]
303 LegalCase,
304 Legislation,
306 Personal,
308 Manuscript,
310 Other,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct Author {
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub given: Option<String>,
321
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub family: Option<String>,
325
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub literal: Option<String>,
329
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub orcid: Option<String>,
333
334 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub affiliation: Option<String>,
337}
338
339impl Author {
340 #[must_use]
342 pub fn new(given: impl Into<String>, family: impl Into<String>) -> Self {
343 Self {
344 given: Some(given.into()),
345 family: Some(family.into()),
346 literal: None,
347 orcid: None,
348 affiliation: None,
349 }
350 }
351
352 #[must_use]
354 pub fn literal(name: impl Into<String>) -> Self {
355 Self {
356 given: None,
357 family: None,
358 literal: Some(name.into()),
359 orcid: None,
360 affiliation: None,
361 }
362 }
363
364 #[must_use]
366 pub fn with_orcid(mut self, orcid: impl Into<String>) -> Self {
367 self.orcid = Some(orcid.into());
368 self
369 }
370
371 #[must_use]
373 pub fn with_affiliation(mut self, affiliation: impl Into<String>) -> Self {
374 self.affiliation = Some(affiliation.into());
375 self
376 }
377
378 #[must_use]
380 pub fn display_name(&self) -> String {
381 if let Some(literal) = &self.literal {
382 return literal.clone();
383 }
384 match (&self.family, &self.given) {
385 (Some(family), Some(given)) => format!("{family}, {given}"),
386 (Some(family), None) => family.clone(),
387 (None, Some(given)) => given.clone(),
388 (None, None) => String::new(),
389 }
390 }
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395#[serde(rename_all = "camelCase")]
396pub struct PartialDate {
397 pub year: i32,
399
400 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub month: Option<u8>,
403
404 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub day: Option<u8>,
407
408 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub season: Option<String>,
411}
412
413impl PartialDate {
414 #[must_use]
416 pub const fn year(year: i32) -> Self {
417 Self {
418 year,
419 month: None,
420 day: None,
421 season: None,
422 }
423 }
424
425 #[must_use]
430 pub const fn year_month(year: i32, month: u8) -> Self {
431 Self {
432 year,
433 month: Some(month),
434 day: None,
435 season: None,
436 }
437 }
438
439 pub fn try_year_month(year: i32, month: u8) -> Result<Self, String> {
445 if !(1..=12).contains(&month) {
446 return Err(format!("month must be 1-12, got {month}"));
447 }
448 Ok(Self::year_month(year, month))
449 }
450
451 #[must_use]
456 pub const fn full(year: i32, month: u8, day: u8) -> Self {
457 Self {
458 year,
459 month: Some(month),
460 day: Some(day),
461 season: None,
462 }
463 }
464
465 pub fn try_full(year: i32, month: u8, day: u8) -> Result<Self, String> {
471 if !(1..=12).contains(&month) {
472 return Err(format!("month must be 1-12, got {month}"));
473 }
474 if !(1..=31).contains(&day) {
475 return Err(format!("day must be 1-31, got {day}"));
476 }
477 Ok(Self::full(year, month, day))
478 }
479
480 #[must_use]
482 pub fn seasonal(year: i32, season: impl Into<String>) -> Self {
483 Self {
484 year,
485 month: None,
486 day: None,
487 season: Some(season.into()),
488 }
489 }
490}
491
492impl<'de> Deserialize<'de> for PartialDate {
493 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
494 where
495 D: serde::Deserializer<'de>,
496 {
497 #[derive(Deserialize)]
498 #[serde(rename_all = "camelCase")]
499 struct Raw {
500 year: i32,
501 #[serde(default)]
502 month: Option<u8>,
503 #[serde(default)]
504 day: Option<u8>,
505 #[serde(default)]
506 season: Option<String>,
507 }
508 let raw = Raw::deserialize(deserializer)?;
509 if let Some(m) = raw.month {
510 if !(1..=12).contains(&m) {
511 return Err(serde::de::Error::custom(format!(
512 "month must be 1-12, got {m}"
513 )));
514 }
515 }
516 if let Some(d) = raw.day {
517 if !(1..=31).contains(&d) {
518 return Err(serde::de::Error::custom(format!(
519 "day must be 1-31, got {d}"
520 )));
521 }
522 }
523 Ok(PartialDate {
524 year: raw.year,
525 month: raw.month,
526 day: raw.day,
527 season: raw.season,
528 })
529 }
530}
531
532impl std::fmt::Display for PartialDate {
533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534 if let Some(season) = &self.season {
535 return write!(f, "{} {}", season, self.year);
536 }
537 match (self.month, self.day) {
538 (Some(month), Some(day)) => write!(f, "{}-{:02}-{:02}", self.year, month, day),
539 (Some(month), None) => write!(f, "{}-{:02}", self.year, month),
540 _ => write!(f, "{}", self.year),
541 }
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_try_year_month_valid() {
551 assert!(PartialDate::try_year_month(2024, 1).is_ok());
552 assert!(PartialDate::try_year_month(2024, 12).is_ok());
553 }
554
555 #[test]
556 fn test_try_year_month_invalid() {
557 assert!(PartialDate::try_year_month(2024, 0).is_err());
558 assert!(PartialDate::try_year_month(2024, 13).is_err());
559 }
560
561 #[test]
562 fn test_try_full_valid() {
563 assert!(PartialDate::try_full(2024, 6, 15).is_ok());
564 }
565
566 #[test]
567 fn test_try_full_invalid() {
568 assert!(PartialDate::try_full(2024, 0, 15).is_err());
569 assert!(PartialDate::try_full(2024, 6, 0).is_err());
570 assert!(PartialDate::try_full(2024, 6, 32).is_err());
571 }
572
573 #[test]
574 fn test_partial_date_deser_rejects_invalid_month() {
575 let json = r#"{"year":2024,"month":13}"#;
576 let result: Result<PartialDate, _> = serde_json::from_str(json);
577 assert!(result.is_err());
578 }
579
580 #[test]
581 fn test_partial_date_deser_rejects_invalid_day() {
582 let json = r#"{"year":2024,"month":6,"day":32}"#;
583 let result: Result<PartialDate, _> = serde_json::from_str(json);
584 assert!(result.is_err());
585 }
586
587 #[test]
588 fn test_partial_date_deser_accepts_valid() {
589 let json = r#"{"year":2024,"month":6,"day":15}"#;
590 let result: PartialDate = serde_json::from_str(json).unwrap();
591 assert_eq!(result.year, 2024);
592 assert_eq!(result.month, Some(6));
593 assert_eq!(result.day, Some(15));
594 }
595}