pubmed_client/europe_pmc/
id.rs1use std::fmt;
8use std::str::FromStr;
9
10use crate::common::PmcId;
11use crate::error::{PubMedError, Result};
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub enum EuropePmcSource {
20 Med,
22 Pmc,
24 Ppr,
26 Agr,
28 Cba,
30 Pat,
32 Other(String),
35}
36
37impl EuropePmcSource {
38 pub fn as_str(&self) -> &str {
40 match self {
41 EuropePmcSource::Med => "MED",
42 EuropePmcSource::Pmc => "PMC",
43 EuropePmcSource::Ppr => "PPR",
44 EuropePmcSource::Agr => "AGR",
45 EuropePmcSource::Cba => "CBA",
46 EuropePmcSource::Pat => "PAT",
47 EuropePmcSource::Other(code) => code,
48 }
49 }
50}
51
52impl fmt::Display for EuropePmcSource {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(self.as_str())
55 }
56}
57
58impl FromStr for EuropePmcSource {
59 type Err = PubMedError;
63
64 fn from_str(s: &str) -> Result<Self> {
65 let upper = s.trim().to_ascii_uppercase();
66 Ok(match upper.as_str() {
67 "MED" => EuropePmcSource::Med,
68 "PMC" => EuropePmcSource::Pmc,
69 "PPR" => EuropePmcSource::Ppr,
70 "AGR" => EuropePmcSource::Agr,
71 "CBA" => EuropePmcSource::Cba,
72 "PAT" => EuropePmcSource::Pat,
73 _ => EuropePmcSource::Other(upper),
74 })
75 }
76}
77
78impl From<&str> for EuropePmcSource {
79 fn from(s: &str) -> Self {
80 s.parse()
82 .unwrap_or_else(|_| EuropePmcSource::Other(s.trim().to_ascii_uppercase()))
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Hash)]
88pub struct EuropePmcId {
89 pub source: EuropePmcSource,
91 pub id: String,
94}
95
96impl EuropePmcId {
97 pub fn new(source: EuropePmcSource, id: impl Into<String>) -> Self {
99 Self {
100 source,
101 id: id.into(),
102 }
103 }
104
105 pub fn pmc(id: &str) -> Result<Self> {
113 let pmc_id = PmcId::parse(id)?;
114 Ok(Self {
115 source: EuropePmcSource::Pmc,
116 id: pmc_id.as_str(),
117 })
118 }
119
120 pub fn med(pmid: impl Into<String>) -> Self {
122 Self {
123 source: EuropePmcSource::Med,
124 id: pmid.into(),
125 }
126 }
127
128 pub fn resolve(id: &str, source: Option<&str>) -> Result<Self> {
167 let id = id.trim();
168 if id.is_empty() {
169 return Err(PubMedError::InvalidQuery(
170 "Europe PMC id must not be empty".to_string(),
171 ));
172 }
173
174 if id.contains('/') {
175 return id.parse();
176 }
177
178 let source = match source {
179 Some(source) if !source.trim().is_empty() => EuropePmcSource::from(source),
180 _ if id.to_ascii_uppercase().starts_with("PMC") => EuropePmcSource::Pmc,
181 _ => EuropePmcSource::Med,
182 };
183
184 if source == EuropePmcSource::Pmc {
185 return Self::pmc(id);
186 }
187
188 Ok(Self::new(source, id))
189 }
190
191 pub(crate) fn pmcid(&self) -> Option<String> {
193 match self.source {
194 EuropePmcSource::Pmc => Some(self.id.clone()),
195 _ => None,
196 }
197 }
198}
199
200impl fmt::Display for EuropePmcId {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{}/{}", self.source, self.id)
203 }
204}
205
206impl FromStr for EuropePmcId {
207 type Err = PubMedError;
208
209 fn from_str(s: &str) -> Result<Self> {
211 let (source, id) = s.trim().split_once('/').ok_or_else(|| {
212 PubMedError::InvalidQuery(format!(
213 "invalid Europe PMC id {s:?}: expected \"SOURCE/ID\" form"
214 ))
215 })?;
216 if id.is_empty() {
217 return Err(PubMedError::InvalidQuery(format!(
218 "invalid Europe PMC id {s:?}: empty record id"
219 )));
220 }
221 Ok(Self {
222 source: EuropePmcSource::from(source),
223 id: id.to_string(),
224 })
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_source_roundtrip() {
234 assert_eq!(EuropePmcSource::Med.as_str(), "MED");
235 assert_eq!(
236 "pmc".parse::<EuropePmcSource>().unwrap(),
237 EuropePmcSource::Pmc
238 );
239 assert_eq!(
240 "xyz".parse::<EuropePmcSource>().unwrap(),
241 EuropePmcSource::Other("XYZ".to_string())
242 );
243 }
244
245 #[test]
246 fn test_pmc_normalizes() {
247 let id = EuropePmcId::pmc("3258128").unwrap();
248 assert_eq!(id.source, EuropePmcSource::Pmc);
249 assert_eq!(id.id, "PMC3258128");
250 assert_eq!(id.to_string(), "PMC/PMC3258128");
251 assert_eq!(id.pmcid().as_deref(), Some("PMC3258128"));
252 }
253
254 #[test]
255 fn test_med_has_no_pmcid() {
256 let id = EuropePmcId::med("12345");
257 assert_eq!(id.to_string(), "MED/12345");
258 assert!(id.pmcid().is_none());
259 }
260
261 #[test]
262 fn test_parse_from_str() {
263 let id: EuropePmcId = "PMC/PMC3258128".parse().unwrap();
264 assert_eq!(id.source, EuropePmcSource::Pmc);
265 assert_eq!(id.id, "PMC3258128");
266
267 let med: EuropePmcId = "MED/12345".parse().unwrap();
268 assert_eq!(med.source, EuropePmcSource::Med);
269
270 assert!("nodelimiter".parse::<EuropePmcId>().is_err());
271 assert!("PMC/".parse::<EuropePmcId>().is_err());
272 }
273
274 #[test]
275 fn test_resolve_bare_pmc_id_defaults_to_pmc_source() {
276 let id = EuropePmcId::resolve("PMC3258128", None).unwrap();
277 assert_eq!(id.to_string(), "PMC/PMC3258128");
278 }
279
280 #[test]
281 fn test_resolve_bare_numeric_id_defaults_to_med_source() {
282 let id = EuropePmcId::resolve("33515491", None).unwrap();
283 assert_eq!(id.to_string(), "MED/33515491");
284 }
285
286 #[test]
287 fn test_resolve_explicit_pmc_source_normalizes_a_bare_number() {
288 assert_eq!(
289 EuropePmcId::resolve("3258128", Some("pmc"))
290 .unwrap()
291 .to_string(),
292 "PMC/PMC3258128"
293 );
294 }
295
296 #[test]
297 fn test_resolve_qualified_id_wins_over_source_argument() {
298 let id = EuropePmcId::resolve("PPR/PPR123456", Some("MED")).unwrap();
299 assert_eq!(id.source, EuropePmcSource::Ppr);
300 }
301
302 #[test]
303 fn test_resolve_blank_source_falls_back_to_the_bare_id_rule() {
304 let id = EuropePmcId::resolve("PMC3258128", Some(" ")).unwrap();
305 assert_eq!(id.source, EuropePmcSource::Pmc);
306 }
307
308 #[test]
309 fn test_resolve_rejects_invalid_ids() {
310 assert!(EuropePmcId::resolve(" ", None).is_err());
311 assert!(EuropePmcId::resolve("MED/", None).is_err());
312 }
313}