1use super::*;
2
3#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6#[cfg_attr(feature = "cv", derive(bincode::Encode, bincode::Decode))]
7#[repr(u8)]
8pub enum ControlledVocabulary {
9 MS = 1,
11 UO,
13 EFO,
15 OBI,
17 HANCESTRO,
19 BFO,
21 NCIT,
23 BTO,
25 PRIDE,
27 #[cfg(feature = "imzml")]
29 IMS,
30 Unknown,
31}
32
33const MS_CV: &str = "MS";
34const UO_CV: &str = "UO";
35const EFO_CV: &str = "EFO";
36const OBI_CV: &str = "OBI";
37const HANCESTRO_CV: &str = "HANCESTRO";
38const BFO_CV: &str = "BFO";
39const BTO_CV: &str = "BTO";
40const NCIT_CV: &str = "NCIT";
41const PRIDE_CV: &str = "PRIDE";
42#[cfg(feature = "imzml")]
43const IMS_CV: &str = "IMS";
44
45const MS_CV_BYTES: &[u8] = MS_CV.as_bytes();
46const UO_CV_BYTES: &[u8] = UO_CV.as_bytes();
47const EFO_CV_BYTES: &[u8] = EFO_CV.as_bytes();
48const OBI_CV_BYTES: &[u8] = OBI_CV.as_bytes();
49const HANCESTRO_CV_BYTES: &[u8] = HANCESTRO_CV.as_bytes();
50const BFO_CV_BYTES: &[u8] = BFO_CV.as_bytes();
51const BTO_CV_BYTES: &[u8] = BTO_CV.as_bytes();
52const NCIT_CV_BYTES: &[u8] = NCIT_CV.as_bytes();
53const PRIDE_CV_BYTES: &[u8] = PRIDE_CV.as_bytes();
54#[cfg(feature = "imzml")]
55const IMS_CV_BYTES: &[u8] = IMS_CV.as_bytes();
56
57impl TryFrom<u8> for ControlledVocabulary {
58 type Error = ControlledVocabularyResolutionError;
59
60 fn try_from(value: u8) -> Result<Self, Self::Error> {
61 match value {
62 1 => Ok(Self::MS),
63 2 => Ok(Self::UO),
64 3 => Ok(Self::EFO),
65 4 => Ok(Self::OBI),
66 5 => Ok(Self::HANCESTRO),
67 6 => Ok(Self::BFO),
68 7 => Ok(Self::NCIT),
69 8 => Ok(Self::BTO),
70 9 => Ok(Self::PRIDE),
71 #[cfg(feature = "imzml")]
72 10 => Ok(Self::IMS),
73 _ => {
74 Err(ControlledVocabularyResolutionError::UnknownControlledVocabularyCode(value))
75 }
76 }
77 }
78}
79
80impl<'a> ControlledVocabulary {
81 pub const fn prefix(&self) -> Cow<'static, str> {
83 match &self {
84 Self::MS => Cow::Borrowed(MS_CV),
85 Self::UO => Cow::Borrowed(UO_CV),
86 Self::EFO => Cow::Borrowed(EFO_CV),
87 Self::OBI => Cow::Borrowed(OBI_CV),
88 Self::HANCESTRO => Cow::Borrowed(HANCESTRO_CV),
89 Self::BFO => Cow::Borrowed(BFO_CV),
90 Self::NCIT => Cow::Borrowed(NCIT_CV),
91 Self::BTO => Cow::Borrowed(BTO_CV),
92 Self::PRIDE => Cow::Borrowed(PRIDE_CV),
93 #[cfg(feature = "imzml")]
94 Self::IMS => Cow::Borrowed(IMS_CV),
95 Self::Unknown => panic!("Cannot encode unknown CV"),
96 }
97 }
98
99 pub const fn as_bytes(&self) -> &'static [u8] {
101 match &self {
102 Self::MS => MS_CV_BYTES,
103 Self::UO => UO_CV_BYTES,
104 Self::EFO => EFO_CV_BYTES,
105 Self::OBI => OBI_CV_BYTES,
106 Self::HANCESTRO => HANCESTRO_CV_BYTES,
107 Self::BFO => BFO_CV_BYTES,
108 Self::NCIT => NCIT_CV_BYTES,
109 Self::BTO => BTO_CV_BYTES,
110 Self::PRIDE => PRIDE_CV_BYTES,
111 #[cfg(feature = "imzml")]
112 Self::IMS => IMS_CV_BYTES,
113 Self::Unknown => panic!("Cannot encode unknown CV"),
114 }
115 }
116
117 pub const fn as_option(&self) -> Option<Self> {
118 match self {
119 Self::Unknown => None,
120 _ => Some(*self),
121 }
122 }
123
124 pub fn param<A: Into<AccessionLike<'a>>, S: Into<String>>(
134 &self,
135 accession: A,
136 name: S,
137 ) -> Param {
138 let mut param = Param::new();
139 param.controlled_vocabulary = Some(*self);
140 param.name = name.into();
141
142 let accession: AccessionLike = accession.into();
143
144 match accession {
145 AccessionLike::Text(s) => {
146 if let Some(nb) = s.split(':').next_back() {
147 param.accession = Some(nb.parse().unwrap_or_else(|_| {
148 panic!("Expected accession to be numeric, got {}", s)
149 }))
150 }
151 }
152 AccessionLike::Number(n) => param.accession = Some(n),
153 AccessionLike::CURIE(c) => param.accession = Some(c.accession),
154 }
155 param
156 }
157
158 pub const fn curie(&self, accession: AccessionIntCode) -> CURIE {
159 CURIE::new(*self, accession)
160 }
161
162 pub const fn const_param(
173 &self,
174 name: &'static str,
175 value: ValueRef<'static>,
176 accession: AccessionIntCode,
177 unit: Unit,
178 ) -> ParamCow<'static> {
179 ParamCow {
180 name: Cow::Borrowed(name),
181 value,
182 accession: Some(accession),
183 controlled_vocabulary: Some(*self),
184 unit,
185 }
186 }
187
188 pub const fn const_param_ident(
193 &self,
194 name: &'static str,
195 accession: AccessionIntCode,
196 ) -> ParamCow<'static> {
197 self.const_param(name, ValueRef::Empty, accession, Unit::Unknown)
198 }
199
200 pub const fn const_param_ident_unit(
207 &self,
208 name: &'static str,
209 accession: AccessionIntCode,
210 unit: Unit,
211 ) -> ParamCow<'static> {
212 self.const_param(name, ValueRef::Empty, accession, unit)
213 }
214
215 pub fn param_val<S: Into<String>, A: Into<AccessionLike<'a>>, V: Into<Value>>(
227 &self,
228 accession: A,
229 name: S,
230 value: V,
231 ) -> Param {
232 let mut param = self.param(accession, name);
233 param.value = value.into();
234 param
235 }
236}
237
238#[derive(Debug, Clone, Error)]
241pub enum ControlledVocabularyResolutionError {
242 #[error("Unrecognized controlled vocabulary {0}")]
243 UnknownControlledVocabulary(String),
244 #[error("Unrecognized controlled vocabulary code {0}")]
245 UnknownControlledVocabularyCode(u8),
246}
247
248impl FromStr for ControlledVocabulary {
249 type Err = ControlledVocabularyResolutionError;
250
251 fn from_str(s: &str) -> Result<Self, Self::Err> {
252 match s {
253 "MS" | "PSI-MS" => Ok(Self::MS),
254 "UO" => Ok(Self::UO),
255 EFO_CV => Ok(Self::EFO),
256 OBI_CV => Ok(Self::OBI),
257 BFO_CV => Ok(Self::BFO),
258 HANCESTRO_CV => Ok(Self::HANCESTRO),
259 #[cfg(feature = "imzml")]
260 IMS_CV => Ok(Self::IMS),
261 _ => Ok(Self::Unknown),
262 }
263 }
264}
265
266pub type AccessionIntCode = u32;
267pub type AccessionByteCode7 = [u8; 7];
268
269#[allow(unused)]
270#[derive(Debug)]
271#[repr(u8)]
272enum AccessionCode {
273 Int(AccessionIntCode),
274 Byte7(AccessionByteCode7),
275}
276
277impl Display for AccessionCode {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 match self {
280 AccessionCode::Int(v) => write!(f, "{v:07}"),
281 AccessionCode::Byte7(v) => write!(
282 f,
283 "{}",
284 core::str::from_utf8(v)
285 .map_err(|e| format!("ERROR:{e}"))
286 .unwrap()
287 ),
288 }
289 }
290}
291
292#[derive(Debug, thiserror::Error, Clone, PartialEq)]
293pub enum AccessionCodeParseError {
294 #[error("The acccession code was too long: {0}")]
295 AccessionCodeTooLong(String),
296 #[error("The acccession code was not in range: {0}")]
297 AccessionCodeNotInRange(String),
298}
299
300impl FromStr for AccessionCode {
301 type Err = AccessionCodeParseError;
302
303 fn from_str(s: &str) -> Result<Self, Self::Err> {
304 if s.len() > 7 {
305 return Err(AccessionCodeParseError::AccessionCodeTooLong(s.to_string()));
306 }
307 if !s.is_ascii() {
308 return Err(AccessionCodeParseError::AccessionCodeNotInRange(
309 s.to_string(),
310 ));
311 }
312 if let Ok(u) = s.parse::<AccessionIntCode>() {
313 Ok(Self::Int(u))
314 } else {
315 let mut bytes = AccessionByteCode7::default();
316 for (byte_from, byte_to) in s.as_bytes().iter().rev().zip(bytes.iter_mut().rev()) {
317 *byte_to = *byte_from;
318 }
319 Ok(Self::Byte7(bytes))
320 }
321 }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
326#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
327#[cfg_attr(feature = "cv", derive(bincode::Encode, bincode::Decode))]
328pub struct CURIE {
329 pub controlled_vocabulary: ControlledVocabulary,
330 pub accession: AccessionIntCode,
331}
332
333impl CURIE {
334 pub const fn new(cv_id: ControlledVocabulary, accession: AccessionIntCode) -> Self {
335 Self {
336 controlled_vocabulary: cv_id,
337 accession,
338 }
339 }
340
341 pub fn as_param(&self) -> Param {
342 let mut param = Param::new();
343 param.controlled_vocabulary = Some(self.controlled_vocabulary);
344 param.accession = Some(self.accession);
345 param
346 }
347
348 #[inline(always)]
349 pub fn accession_int(&self) -> u32 {
350 self.accession
351 }
352
353 #[inline(always)]
354 pub fn controlled_vocabulary(&self) -> ControlledVocabulary {
355 self.controlled_vocabulary
356 }
357}
358
359impl Display for CURIE {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 write!(
362 f,
363 "{}:{:07}",
364 self.controlled_vocabulary.prefix(),
365 self.accession
366 )
367 }
368}
369
370impl<T: ParamLike> PartialEq<T> for CURIE {
371 fn eq(&self, other: &T) -> bool {
372 if !other.is_controlled()
373 || other
374 .controlled_vocabulary()
375 .map(|c| c != self.controlled_vocabulary)
376 .unwrap_or_default()
377 {
378 false
379 } else {
380 other
381 .accession()
382 .map(|a| a == self.accession)
383 .unwrap_or_default()
384 }
385 }
386}
387
388#[derive(Debug, Error)]
389pub enum CURIEParsingError {
390 #[error("{0} is not a recognized controlled vocabulary")]
391 UnknownControlledVocabulary(
392 #[from]
393 #[source]
394 ControlledVocabularyResolutionError,
395 ),
396 #[error("Failed to parse accession number {0}")]
397 AccessionParsingError(
398 #[from]
399 #[source]
400 num::ParseIntError,
401 ),
402 #[error("Did not detect a namespace separator ':' token")]
403 MissingNamespaceSeparator,
404}
405
406impl FromStr for CURIE {
407 type Err = CURIEParsingError;
408
409 fn from_str(s: &str) -> Result<Self, Self::Err> {
410 let mut tokens = s.split(':');
411 let cv = tokens
412 .next()
413 .ok_or(CURIEParsingError::MissingNamespaceSeparator)?;
414 let accession = tokens.next();
415 if accession.is_none() {
416 Err(CURIEParsingError::MissingNamespaceSeparator)
417 } else {
418 let cv: ControlledVocabulary = cv.parse::<ControlledVocabulary>()?;
419
420 let accession = accession.unwrap().parse()?;
421 Ok(CURIE::new(cv, accession))
422 }
423 }
424}
425
426impl TryFrom<&Param> for CURIE {
427 type Error = String;
428
429 fn try_from(value: &Param) -> Result<Self, Self::Error> {
430 match (value.controlled_vocabulary, value.accession) {
431 (Some(cv), Some(acc)) => Ok(CURIE::new(cv, acc)),
432 _ => Err(format!(
433 "{} is missing controlled vocabulary or accession",
434 value.name()
435 )),
436 }
437 }
438}
439
440impl<'a> TryFrom<&ParamCow<'a>> for CURIE {
441 type Error = String;
442
443 fn try_from(value: &ParamCow<'a>) -> Result<Self, Self::Error> {
444 match (value.controlled_vocabulary, value.accession) {
445 (Some(cv), Some(acc)) => Ok(CURIE::new(cv, acc)),
446 _ => Err(format!(
447 "{} is missing controlled vocabulary or accession",
448 value.name()
449 )),
450 }
451 }
452}
453
454pub fn curie_to_num(curie: &str) -> (Option<ControlledVocabulary>, Option<AccessionIntCode>) {
455 let mut parts = curie.split(':');
456 let prefix = match parts.next() {
457 Some(v) => v.parse::<ControlledVocabulary>().unwrap().as_option(),
458 None => None,
459 };
460 if let Some(k) = curie.split(':').nth(1) {
461 match k.parse() {
462 Ok(v) => (prefix, Some(v)),
463 Err(_) => (prefix, None),
464 }
465 } else {
466 (prefix, None)
467 }
468}