1use std::collections::BTreeMap;
11use std::fmt;
12
13macro_rules! csl_vocab {
18 (
19 $(#[$meta:meta])*
20 $vis:vis enum $name:ident { $($(#[$variant_meta:meta])* $variant:ident => $text:literal),+ $(,)? }
21 ) => {
22 $(#[$meta])*
23 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
24 $vis enum $name {
25 $($(#[$variant_meta])* $variant),+
26 }
27
28 impl $name {
29 #[must_use]
31 pub const fn as_str(self) -> &'static str {
32 match self {
33 $(Self::$variant => $text,)+
34 }
35 }
36
37 #[must_use]
39 pub fn from_csl(text: &str) -> Option<Self> {
40 match text {
41 $($text => Some(Self::$variant),)+
42 _ => None,
43 }
44 }
45 }
46
47 impl fmt::Display for $name {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(self.as_str())
50 }
51 }
52 };
53}
54
55csl_vocab! {
56 pub enum ItemType {
71 Article => "article",
72 ArticleJournal => "article-journal",
73 ArticleMagazine => "article-magazine",
74 ArticleNewspaper => "article-newspaper",
75 Bill => "bill",
76 Book => "book",
77 Broadcast => "broadcast",
78 Chapter => "chapter",
79 Classic => "classic",
80 Collection => "collection",
81 Dataset => "dataset",
82 Document => "document",
83 Entry => "entry",
84 EntryDictionary => "entry-dictionary",
85 EntryEncyclopedia => "entry-encyclopedia",
86 Event => "event",
87 Figure => "figure",
88 Graphic => "graphic",
89 Hearing => "hearing",
90 Interview => "interview",
91 LegalCase => "legal_case",
92 Legislation => "legislation",
93 Manuscript => "manuscript",
94 Map => "map",
95 MotionPicture => "motion_picture",
96 MusicalScore => "musical_score",
97 Pamphlet => "pamphlet",
98 PaperConference => "paper-conference",
99 Patent => "patent",
100 Performance => "performance",
101 Periodical => "periodical",
102 PersonalCommunication => "personal_communication",
103 Post => "post",
104 PostWeblog => "post-weblog",
105 Regulation => "regulation",
106 Report => "report",
107 Review => "review",
108 ReviewBook => "review-book",
109 Software => "software",
110 Song => "song",
111 Speech => "speech",
112 Standard => "standard",
113 Thesis => "thesis",
114 Treaty => "treaty",
115 Webpage => "webpage",
116 }
117}
118
119csl_vocab! {
120 pub enum StandardVariable {
122 Abstract => "abstract",
123 Annote => "annote",
124 Archive => "archive",
125 ArchiveCollection => "archive_collection",
126 ArchiveLocation => "archive_location",
127 ArchivePlace => "archive-place",
128 Authority => "authority",
129 CallNumber => "call-number",
130 CitationKey => "citation-key",
131 CitationLabel => "citation-label",
132 CollectionTitle => "collection-title",
133 ContainerTitle => "container-title",
134 ContainerTitleShort => "container-title-short",
135 Dimensions => "dimensions",
136 Division => "division",
137 Doi => "DOI",
138 Event => "event",
140 EventTitle => "event-title",
141 EventPlace => "event-place",
142 Genre => "genre",
143 Isbn => "ISBN",
144 Issn => "ISSN",
145 Jurisdiction => "jurisdiction",
146 Keyword => "keyword",
147 Language => "language",
148 License => "license",
149 Medium => "medium",
150 Note => "note",
151 OriginalPublisher => "original-publisher",
152 OriginalPublisherPlace => "original-publisher-place",
153 OriginalTitle => "original-title",
154 PartTitle => "part-title",
155 Pmcid => "PMCID",
156 Pmid => "PMID",
157 Publisher => "publisher",
158 PublisherPlace => "publisher-place",
159 References => "references",
160 ReviewedGenre => "reviewed-genre",
161 ReviewedTitle => "reviewed-title",
162 Scale => "scale",
163 Source => "source",
164 Status => "status",
165 Title => "title",
166 TitleShort => "title-short",
167 Url => "URL",
168 VolumeTitle => "volume-title",
169 YearSuffix => "year-suffix",
170 }
171}
172
173csl_vocab! {
174 pub enum NumberVariable {
179 ChapterNumber => "chapter-number",
180 CitationNumber => "citation-number",
181 CollectionNumber => "collection-number",
182 Edition => "edition",
183 FirstReferenceNoteNumber => "first-reference-note-number",
184 Issue => "issue",
185 Locator => "locator",
186 Number => "number",
187 NumberOfPages => "number-of-pages",
188 NumberOfVolumes => "number-of-volumes",
189 Page => "page",
190 PageFirst => "page-first",
191 PartNumber => "part-number",
192 PrintingNumber => "printing-number",
193 Section => "section",
194 SupplementNumber => "supplement-number",
195 Version => "version",
196 Volume => "volume",
197 }
198}
199
200csl_vocab! {
201 pub enum DateVariable {
203 Accessed => "accessed",
204 AvailableDate => "available-date",
205 EventDate => "event-date",
206 Issued => "issued",
207 OriginalDate => "original-date",
208 Submitted => "submitted",
209 }
210}
211
212csl_vocab! {
213 pub enum NameVariable {
215 Author => "author",
216 Chair => "chair",
217 CollectionEditor => "collection-editor",
218 Compiler => "compiler",
219 Composer => "composer",
220 ContainerAuthor => "container-author",
221 Contributor => "contributor",
222 Curator => "curator",
223 Director => "director",
224 Editor => "editor",
225 EditorialDirector => "editorial-director",
226 EditorTranslator => "editor-translator",
227 ExecutiveProducer => "executive-producer",
228 Guest => "guest",
229 Host => "host",
230 Illustrator => "illustrator",
231 Interviewer => "interviewer",
232 Narrator => "narrator",
233 Organizer => "organizer",
234 OriginalAuthor => "original-author",
235 Performer => "performer",
236 Producer => "producer",
237 Recipient => "recipient",
238 ReviewedAuthor => "reviewed-author",
239 ScriptWriter => "script-writer",
240 SeriesCreator => "series-creator",
241 Translator => "translator",
242 }
243}
244
245#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
250pub struct Name {
251 pub family: Option<String>,
252 pub given: Option<String>,
253 pub suffix: Option<String>,
254 pub dropping_particle: Option<String>,
255 pub non_dropping_particle: Option<String>,
256 pub literal: Option<String>,
257}
258
259impl Name {
260 #[must_use]
262 pub fn literal(text: impl Into<String>) -> Self {
263 Self {
264 literal: Some(text.into()),
265 ..Self::default()
266 }
267 }
268
269 #[must_use]
271 pub fn person(family: impl Into<String>, given: impl Into<String>) -> Self {
272 Self {
273 family: Some(family.into()),
274 given: Some(given.into()),
275 ..Self::default()
276 }
277 }
278}
279
280#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
282pub struct DateParts {
283 pub year: Option<i32>,
284 pub month: Option<u8>,
285 pub day: Option<u8>,
286 pub season: Option<u8>,
287}
288
289#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
292pub struct Date {
293 pub start: DateParts,
294 pub end: Option<DateParts>,
295 pub circa: bool,
296 pub literal: Option<String>,
297}
298
299impl Date {
300 #[must_use]
302 pub fn year(year: i32) -> Self {
303 Self {
304 start: DateParts {
305 year: Some(year),
306 ..DateParts::default()
307 },
308 ..Self::default()
309 }
310 }
311
312 #[must_use]
314 pub fn literal(text: impl Into<String>) -> Self {
315 Self {
316 literal: Some(text.into()),
317 ..Self::default()
318 }
319 }
320}
321
322#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct Item {
329 pub id: String,
330 pub item_type: ItemType,
331 pub standard: BTreeMap<StandardVariable, String>,
332 pub number: BTreeMap<NumberVariable, String>,
333 pub date: BTreeMap<DateVariable, Date>,
334 pub name: BTreeMap<NameVariable, Vec<Name>>,
335}
336
337impl Default for Item {
338 fn default() -> Self {
339 Self {
342 id: String::new(),
343 item_type: ItemType::Document,
344 standard: BTreeMap::new(),
345 number: BTreeMap::new(),
346 date: BTreeMap::new(),
347 name: BTreeMap::new(),
348 }
349 }
350}
351
352impl Item {
353 #[must_use]
365 pub fn new(id: impl Into<String>, item_type: ItemType) -> Self {
366 Self {
367 id: id.into(),
368 item_type,
369 ..Self::default()
370 }
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn vocab_helpers_round_trip_known_values_and_reject_unknowns() {
380 assert_eq!(ItemType::ArticleJournal.as_str(), "article-journal");
381 assert_eq!(ItemType::from_csl("book"), Some(ItemType::Book));
382 assert_eq!(ItemType::from_csl("unknown"), None);
383 assert_eq!(ItemType::Webpage.to_string(), "webpage");
384
385 assert_eq!(StandardVariable::Doi.as_str(), "DOI");
386 assert_eq!(StandardVariable::Event.as_str(), "event");
387 assert_eq!(
388 StandardVariable::from_csl("container-title"),
389 Some(StandardVariable::ContainerTitle)
390 );
391 assert_eq!(
392 StandardVariable::from_csl("event"),
393 Some(StandardVariable::Event)
394 );
395 assert_eq!(StandardVariable::from_csl("doi"), None);
396 assert_eq!(StandardVariable::Url.to_string(), "URL");
397
398 assert_eq!(NumberVariable::from_csl("page"), Some(NumberVariable::Page));
399 assert_eq!(NumberVariable::from_csl("pages"), None);
400 assert_eq!(NumberVariable::Volume.to_string(), "volume");
401
402 assert_eq!(DateVariable::from_csl("issued"), Some(DateVariable::Issued));
403 assert_eq!(DateVariable::from_csl("published"), None);
404 assert_eq!(DateVariable::Accessed.to_string(), "accessed");
405
406 assert_eq!(NameVariable::from_csl("author"), Some(NameVariable::Author));
407 assert_eq!(NameVariable::from_csl("authors"), None);
408 assert_eq!(NameVariable::Translator.to_string(), "translator");
409 }
410
411 #[test]
412 fn constructors_create_precise_item_values() {
413 let literal = Name::literal("Mosaic Team");
414 assert_eq!(literal.literal.as_deref(), Some("Mosaic Team"));
415 assert_eq!(literal.family, None);
416
417 let person = Name::person("Lovelace", "Ada");
418 assert_eq!(person.family.as_deref(), Some("Lovelace"));
419 assert_eq!(person.given.as_deref(), Some("Ada"));
420
421 let year = Date::year(1843);
422 assert_eq!(year.start.year, Some(1843));
423 assert_eq!(year.literal, None);
424
425 let literal_date = Date::literal("forthcoming");
426 assert_eq!(literal_date.literal.as_deref(), Some("forthcoming"));
427 assert_eq!(literal_date.start.year, None);
428
429 let default_item = Item::default();
430 assert_eq!(default_item.item_type, ItemType::Document);
431 assert!(default_item.standard.is_empty());
432
433 let article = Item::new("ada1843", ItemType::ArticleJournal);
434 assert_eq!(article.id, "ada1843");
435 assert_eq!(article.item_type, ItemType::ArticleJournal);
436 }
437}