oxihuman_export/
ris_export.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq)]
9pub enum RisType {
10 Journal,
11 Book,
12 Conference,
13 Report,
14 Thesis,
15 Generic,
16}
17
18impl RisType {
19 pub fn code(&self) -> &'static str {
21 match self {
22 Self::Journal => "JOUR",
23 Self::Book => "BOOK",
24 Self::Conference => "CONF",
25 Self::Report => "RPRT",
26 Self::Thesis => "THES",
27 Self::Generic => "GEN",
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct RisField {
35 pub tag: String,
36 pub value: String,
37}
38
39#[derive(Debug, Clone)]
41pub struct RisRecord {
42 pub ref_type: RisType,
43 pub fields: Vec<RisField>,
44}
45
46impl RisRecord {
47 pub fn new(ref_type: RisType) -> Self {
49 Self {
50 ref_type,
51 fields: Vec::new(),
52 }
53 }
54
55 pub fn add_field(&mut self, tag: impl Into<String>, value: impl Into<String>) {
57 self.fields.push(RisField {
58 tag: tag.into(),
59 value: value.into(),
60 });
61 }
62
63 pub fn get_field(&self, tag: &str) -> Option<&str> {
65 self.fields
66 .iter()
67 .find(|f| f.tag == tag)
68 .map(|f| f.value.as_str())
69 }
70}
71
72#[derive(Debug, Clone, Default)]
74pub struct RisDatabase {
75 pub records: Vec<RisRecord>,
76}
77
78impl RisDatabase {
79 pub fn add_record(&mut self, record: RisRecord) {
81 self.records.push(record);
82 }
83
84 pub fn record_count(&self) -> usize {
86 self.records.len()
87 }
88}
89
90pub fn render_record(rec: &RisRecord) -> String {
92 let mut out = format!("TY - {}\n", rec.ref_type.code());
93 for field in &rec.fields {
94 out.push_str(&format!("{} - {}\n", field.tag, field.value));
95 }
96 out.push_str("ER - \n");
97 out
98}
99
100pub fn render_ris(db: &RisDatabase) -> String {
102 db.records
103 .iter()
104 .map(render_record)
105 .collect::<Vec<_>>()
106 .join("\n")
107}
108
109pub fn validate_record(rec: &RisRecord) -> bool {
111 rec.get_field("TI").is_some() || rec.get_field("T1").is_some()
112}
113
114pub fn count_by_type(db: &RisDatabase, ref_type: &RisType) -> usize {
116 db.records
117 .iter()
118 .filter(|r| &r.ref_type == ref_type)
119 .count()
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 fn sample_record() -> RisRecord {
127 let mut r = RisRecord::new(RisType::Journal);
128 r.add_field("TI", "Test Title");
129 r.add_field("AU", "Smith, J.");
130 r.add_field("PY", "2026");
131 r
132 }
133
134 #[test]
135 fn type_code() {
136 assert_eq!(RisType::Journal.code(), "JOUR");
137 }
138
139 #[test]
140 fn get_field_found() {
141 assert_eq!(sample_record().get_field("TI"), Some("Test Title"));
142 }
143
144 #[test]
145 fn get_field_missing() {
146 assert!(sample_record().get_field("ZZ").is_none());
147 }
148
149 #[test]
150 fn record_count() {
151 let mut db = RisDatabase::default();
152 db.add_record(sample_record());
153 assert_eq!(db.record_count(), 1);
154 }
155
156 #[test]
157 fn render_record_ty_line() {
158 let s = render_record(&sample_record());
159 assert!(s.starts_with("TY - JOUR"));
160 }
161
162 #[test]
163 fn render_record_er_line() {
164 assert!(render_record(&sample_record()).contains("ER - "));
165 }
166
167 #[test]
168 fn validate_ok() {
169 assert!(validate_record(&sample_record()));
170 }
171
172 #[test]
173 fn validate_no_title() {
174 let r = RisRecord::new(RisType::Generic);
175 assert!(!validate_record(&r));
176 }
177
178 #[test]
179 fn count_by_type_correct() {
180 let mut db = RisDatabase::default();
181 db.add_record(sample_record());
182 db.add_record(RisRecord::new(RisType::Book));
183 assert_eq!(count_by_type(&db, &RisType::Journal), 1);
184 }
185
186 #[test]
187 fn render_ris_contains_type() {
188 let mut db = RisDatabase::default();
189 db.add_record(sample_record());
190 assert!(render_ris(&db).contains("JOUR"));
191 }
192}