Skip to main content

cdx_core/extensions/semantic/
bibliography.rs

1//! Bibliography management for academic documents.
2
3use serde::{Deserialize, Serialize};
4
5/// A bibliography containing all references cited in a document.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Bibliography {
9    /// Citation style used for formatting.
10    #[serde(default)]
11    pub style: CitationStyle,
12
13    /// Bibliography entries.
14    pub entries: Vec<BibliographyEntry>,
15}
16
17impl Bibliography {
18    /// Create a new empty bibliography.
19    #[must_use]
20    pub fn new(style: CitationStyle) -> Self {
21        Self {
22            style,
23            entries: Vec::new(),
24        }
25    }
26
27    /// Add an entry to the bibliography.
28    pub fn add_entry(&mut self, entry: BibliographyEntry) {
29        self.entries.push(entry);
30    }
31
32    /// Find an entry by its ID.
33    #[must_use]
34    pub fn get(&self, id: &str) -> Option<&BibliographyEntry> {
35        self.entries.iter().find(|e| e.id == id)
36    }
37
38    /// Check if the bibliography contains an entry with the given ID.
39    #[must_use]
40    pub fn contains(&self, id: &str) -> bool {
41        self.entries.iter().any(|e| e.id == id)
42    }
43
44    /// Get the number of entries.
45    #[must_use]
46    pub fn len(&self) -> usize {
47        self.entries.len()
48    }
49
50    /// Check if the bibliography is empty.
51    #[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/// Citation style for formatting references.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, strum::Display)]
65#[serde(rename_all = "lowercase")]
66pub enum CitationStyle {
67    /// APA (American Psychological Association) style.
68    #[default]
69    #[strum(serialize = "APA")]
70    Apa,
71    /// MLA (Modern Language Association) style.
72    #[strum(serialize = "MLA")]
73    Mla,
74    /// Chicago Manual of Style.
75    Chicago,
76    /// IEEE style.
77    #[strum(serialize = "IEEE")]
78    Ieee,
79    /// Harvard style.
80    Harvard,
81    /// Vancouver style.
82    Vancouver,
83    /// ACM style.
84    #[strum(serialize = "ACM")]
85    Acm,
86    /// Custom style (implementation-defined).
87    Custom,
88}
89
90/// A bibliography entry representing a single reference.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct BibliographyEntry {
94    /// Unique identifier for the entry (used in citations).
95    pub id: String,
96
97    /// Type of the entry.
98    pub entry_type: EntryType,
99
100    /// Title of the work.
101    pub title: String,
102
103    /// Authors of the work.
104    #[serde(default, skip_serializing_if = "Vec::is_empty")]
105    pub authors: Vec<Author>,
106
107    /// Publication date.
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub issued: Option<PartialDate>,
110
111    /// Container title (e.g., journal name, book title for chapters).
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub container_title: Option<String>,
114
115    /// Volume number.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub volume: Option<String>,
118
119    /// Issue number.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub issue: Option<String>,
122
123    /// Page range.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub page: Option<String>,
126
127    /// Digital Object Identifier.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub doi: Option<String>,
130
131    /// URL to the work.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub url: Option<String>,
134
135    /// ISBN for books.
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub isbn: Option<String>,
138
139    /// ISSN for journals.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub issn: Option<String>,
142
143    /// Publisher name.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub publisher: Option<String>,
146
147    /// Publication location.
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub publisher_place: Option<String>,
150
151    /// Edition number or description.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub edition: Option<String>,
154
155    /// Editors (for edited volumes).
156    #[serde(default, skip_serializing_if = "Vec::is_empty")]
157    pub editors: Vec<Author>,
158
159    /// Abstract text.
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub abstract_text: Option<String>,
162
163    /// Keywords or tags.
164    #[serde(default, skip_serializing_if = "Vec::is_empty")]
165    pub keywords: Vec<String>,
166
167    /// Language of the work.
168    #[serde(default, skip_serializing_if = "Option::is_none")]
169    pub language: Option<String>,
170
171    /// Access date for online resources.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub accessed: Option<PartialDate>,
174
175    /// Additional notes.
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub note: Option<String>,
178}
179
180impl BibliographyEntry {
181    /// Create a new bibliography entry.
182    #[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    /// Add an author.
211    #[must_use]
212    pub fn with_author(mut self, author: Author) -> Self {
213        self.authors.push(author);
214        self
215    }
216
217    /// Add multiple authors.
218    #[must_use]
219    pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
220        self.authors = authors;
221        self
222    }
223
224    /// Set the publication date.
225    #[must_use]
226    pub fn with_issued(mut self, date: PartialDate) -> Self {
227        self.issued = Some(date);
228        self
229    }
230
231    /// Set the container title.
232    #[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    /// Set volume and issue.
239    #[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    /// Set page range.
247    #[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    /// Set DOI.
254    #[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    /// Set URL.
261    #[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    /// Set publisher information.
268    #[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/// Type of bibliography entry.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
278#[serde(rename_all = "camelCase")]
279#[strum(serialize_all = "lowercase")]
280pub enum EntryType {
281    /// Journal article.
282    Article,
283    /// Book.
284    Book,
285    /// Chapter in a book.
286    Chapter,
287    /// Conference paper.
288    Conference,
289    /// Thesis or dissertation.
290    Thesis,
291    /// Technical report.
292    Report,
293    /// Website or webpage.
294    Webpage,
295    /// Patent.
296    Patent,
297    /// Dataset.
298    Dataset,
299    /// Software.
300    Software,
301    /// Legal case.
302    #[strum(serialize = "legal-case")]
303    LegalCase,
304    /// Legislation or statute.
305    Legislation,
306    /// Personal communication.
307    Personal,
308    /// Manuscript.
309    Manuscript,
310    /// Other type.
311    Other,
312}
313
314/// An author or contributor.
315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct Author {
318    /// Given name (first name).
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub given: Option<String>,
321
322    /// Family name (last name).
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub family: Option<String>,
325
326    /// Full literal name (for non-standard names or organizations).
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub literal: Option<String>,
329
330    /// ORCID identifier.
331    #[serde(default, skip_serializing_if = "Option::is_none")]
332    pub orcid: Option<String>,
333
334    /// Affiliation.
335    #[serde(default, skip_serializing_if = "Option::is_none")]
336    pub affiliation: Option<String>,
337}
338
339impl Author {
340    /// Create an author from given and family names.
341    #[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    /// Create an author from a literal name (e.g., organization).
353    #[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    /// Set ORCID.
365    #[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    /// Set affiliation.
372    #[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    /// Get the display name.
379    #[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/// A partial date (year, year-month, or full date).
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub struct PartialDate {
397    /// Year.
398    pub year: i32,
399
400    /// Month (1-12).
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub month: Option<u8>,
403
404    /// Day (1-31).
405    #[serde(default, skip_serializing_if = "Option::is_none")]
406    pub day: Option<u8>,
407
408    /// Season (for quarterly publications).
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub season: Option<String>,
411}
412
413impl PartialDate {
414    /// Create a year-only date.
415    #[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    /// Create a year-month date.
426    #[must_use]
427    pub const fn year_month(year: i32, month: u8) -> Self {
428        Self {
429            year,
430            month: Some(month),
431            day: None,
432            season: None,
433        }
434    }
435
436    /// Create a full date.
437    #[must_use]
438    pub const fn full(year: i32, month: u8, day: u8) -> Self {
439        Self {
440            year,
441            month: Some(month),
442            day: Some(day),
443            season: None,
444        }
445    }
446
447    /// Create a seasonal date.
448    #[must_use]
449    pub fn seasonal(year: i32, season: impl Into<String>) -> Self {
450        Self {
451            year,
452            month: None,
453            day: None,
454            season: Some(season.into()),
455        }
456    }
457}
458
459impl std::fmt::Display for PartialDate {
460    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        if let Some(season) = &self.season {
462            return write!(f, "{} {}", season, self.year);
463        }
464        match (self.month, self.day) {
465            (Some(month), Some(day)) => write!(f, "{}-{:02}-{:02}", self.year, month, day),
466            (Some(month), None) => write!(f, "{}-{:02}", self.year, month),
467            _ => write!(f, "{}", self.year),
468        }
469    }
470}