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,
35}
36
37const MS_CV: &str = "MS";
38const UO_CV: &str = "UO";
39const EFO_CV: &str = "EFO";
40const OBI_CV: &str = "OBI";
41const HANCESTRO_CV: &str = "HANCESTRO";
42const BFO_CV: &str = "BFO";
43const BTO_CV: &str = "BTO";
44const NCIT_CV: &str = "NCIT";
45const PRIDE_CV: &str = "PRIDE";
46#[cfg(feature = "imzml")]
47const IMS_CV: &str = "IMS";
48
49const MS_CV_BYTES: &[u8] = MS_CV.as_bytes();
50const UO_CV_BYTES: &[u8] = UO_CV.as_bytes();
51const EFO_CV_BYTES: &[u8] = EFO_CV.as_bytes();
52const OBI_CV_BYTES: &[u8] = OBI_CV.as_bytes();
53const HANCESTRO_CV_BYTES: &[u8] = HANCESTRO_CV.as_bytes();
54const BFO_CV_BYTES: &[u8] = BFO_CV.as_bytes();
55const BTO_CV_BYTES: &[u8] = BTO_CV.as_bytes();
56const NCIT_CV_BYTES: &[u8] = NCIT_CV.as_bytes();
57const PRIDE_CV_BYTES: &[u8] = PRIDE_CV.as_bytes();
58#[cfg(feature = "imzml")]
59const IMS_CV_BYTES: &[u8] = IMS_CV.as_bytes();
60
61impl TryFrom<u8> for ControlledVocabulary {
62 type Error = ControlledVocabularyResolutionError;
63
64 fn try_from(value: u8) -> Result<Self, Self::Error> {
65 match value {
66 1 => Ok(Self::MS),
67 2 => Ok(Self::UO),
68 3 => Ok(Self::EFO),
69 4 => Ok(Self::OBI),
70 5 => Ok(Self::HANCESTRO),
71 6 => Ok(Self::BFO),
72 7 => Ok(Self::NCIT),
73 8 => Ok(Self::BTO),
74 9 => Ok(Self::PRIDE),
75 #[cfg(feature = "imzml")]
76 10 => Ok(Self::IMS),
77 _ => {
78 Err(ControlledVocabularyResolutionError::UnknownControlledVocabularyCode(value))
79 }
80 }
81 }
82}
83
84impl<'a> ControlledVocabulary {
85 pub const fn prefix(&self) -> Cow<'static, str> {
87 match &self {
88 Self::MS => Cow::Borrowed(MS_CV),
89 Self::UO => Cow::Borrowed(UO_CV),
90 Self::EFO => Cow::Borrowed(EFO_CV),
91 Self::OBI => Cow::Borrowed(OBI_CV),
92 Self::HANCESTRO => Cow::Borrowed(HANCESTRO_CV),
93 Self::BFO => Cow::Borrowed(BFO_CV),
94 Self::NCIT => Cow::Borrowed(NCIT_CV),
95 Self::BTO => Cow::Borrowed(BTO_CV),
96 Self::PRIDE => Cow::Borrowed(PRIDE_CV),
97 #[cfg(feature = "imzml")]
98 Self::IMS => Cow::Borrowed(IMS_CV),
99 Self::Unknown => panic!("Cannot encode unknown CV"),
100 }
101 }
102
103 pub const fn as_bytes(&self) -> &'static [u8] {
105 match &self {
106 Self::MS => MS_CV_BYTES,
107 Self::UO => UO_CV_BYTES,
108 Self::EFO => EFO_CV_BYTES,
109 Self::OBI => OBI_CV_BYTES,
110 Self::HANCESTRO => HANCESTRO_CV_BYTES,
111 Self::BFO => BFO_CV_BYTES,
112 Self::NCIT => NCIT_CV_BYTES,
113 Self::BTO => BTO_CV_BYTES,
114 Self::PRIDE => PRIDE_CV_BYTES,
115 #[cfg(feature = "imzml")]
116 Self::IMS => IMS_CV_BYTES,
117 Self::Unknown => panic!("Cannot encode unknown CV"),
118 }
119 }
120
121 pub const fn as_option(&self) -> Option<Self> {
124 match self {
125 Self::Unknown => None,
126 _ => Some(*self),
127 }
128 }
129
130 pub fn param<A: Into<AccessionLike<'a>>, S: Into<String>>(
140 &self,
141 accession: A,
142 name: S,
143 ) -> Param {
144 let mut param = Param::new();
145 param.controlled_vocabulary = Some(*self);
146 param.name = name.into();
147
148 let accession: AccessionLike = accession.into();
149
150 match accession {
151 AccessionLike::Text(s) => {
152 if let Some(nb) = s.split(':').next_back() {
153 param.accession = Some(nb.parse().unwrap_or_else(|_| {
154 panic!("Expected accession to be numeric, got {}", s)
155 }))
156 }
157 }
158 AccessionLike::Number(n) => param.accession = Some(n),
159 AccessionLike::CURIE(c) => param.accession = Some(c.accession),
160 }
161 param
162 }
163
164 pub const fn curie(&self, accession: AccessionIntCode) -> CURIE {
166 CURIE::new(*self, accession)
167 }
168
169 pub const fn const_param(
180 &self,
181 name: &'static str,
182 value: ValueRef<'static>,
183 accession: AccessionIntCode,
184 unit: Unit,
185 ) -> ParamCow<'static> {
186 ParamCow {
187 name: Cow::Borrowed(name),
188 value,
189 accession: Some(accession),
190 controlled_vocabulary: Some(*self),
191 unit,
192 }
193 }
194
195 pub const fn const_param_ident(
200 &self,
201 name: &'static str,
202 accession: AccessionIntCode,
203 ) -> ParamCow<'static> {
204 self.const_param(name, ValueRef::Empty, accession, Unit::Unknown)
205 }
206
207 pub const fn const_param_ident_unit(
214 &self,
215 name: &'static str,
216 accession: AccessionIntCode,
217 unit: Unit,
218 ) -> ParamCow<'static> {
219 self.const_param(name, ValueRef::Empty, accession, unit)
220 }
221
222 pub fn param_val<S: Into<String>, A: Into<AccessionLike<'a>>, V: Into<Value>>(
234 &self,
235 accession: A,
236 name: S,
237 value: V,
238 ) -> Param {
239 let mut param = self.param(accession, name);
240 param.value = value.into();
241 param
242 }
243}
244
245#[derive(Debug, Clone, Error)]
248pub enum ControlledVocabularyResolutionError {
249 #[error("Unrecognized controlled vocabulary {0}")]
251 UnknownControlledVocabulary(String),
252 #[error("Unrecognized controlled vocabulary code {0}")]
255 UnknownControlledVocabularyCode(u8),
256}
257
258impl FromStr for ControlledVocabulary {
259 type Err = ControlledVocabularyResolutionError;
260
261 fn from_str(s: &str) -> Result<Self, Self::Err> {
262 match s {
263 "MS" | "PSI-MS" => Ok(Self::MS),
264 "UO" => Ok(Self::UO),
265 EFO_CV => Ok(Self::EFO),
266 OBI_CV => Ok(Self::OBI),
267 BFO_CV => Ok(Self::BFO),
268 HANCESTRO_CV => Ok(Self::HANCESTRO),
269 #[cfg(feature = "imzml")]
270 IMS_CV => Ok(Self::IMS),
271 _ => Ok(Self::Unknown),
272 }
273 }
274}
275
276pub type AccessionIntCode = u32;
278pub type AccessionByteCode7 = [u8; 7];
281
282#[allow(unused)]
283#[derive(Debug)]
284#[repr(u8)]
285enum AccessionCode {
286 Int(AccessionIntCode),
287 Byte7(AccessionByteCode7),
288}
289
290impl Display for AccessionCode {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 match self {
293 AccessionCode::Int(v) => write!(f, "{v:07}"),
294 AccessionCode::Byte7(v) => write!(
295 f,
296 "{}",
297 core::str::from_utf8(v)
298 .map_err(|e| format!("ERROR:{e}"))
299 .unwrap()
300 ),
301 }
302 }
303}
304
305#[derive(Debug, thiserror::Error, Clone, PartialEq)]
306pub enum AccessionCodeParseError {
307 #[error("The acccession code was too long: {0}")]
308 AccessionCodeTooLong(String),
309 #[error("The acccession code was not in range: {0}")]
310 AccessionCodeNotInRange(String),
311}
312
313impl FromStr for AccessionCode {
314 type Err = AccessionCodeParseError;
315
316 fn from_str(s: &str) -> Result<Self, Self::Err> {
317 if s.len() > 7 {
318 return Err(AccessionCodeParseError::AccessionCodeTooLong(s.to_string()));
319 }
320 if !s.is_ascii() {
321 return Err(AccessionCodeParseError::AccessionCodeNotInRange(
322 s.to_string(),
323 ));
324 }
325 if let Ok(u) = s.parse::<AccessionIntCode>() {
326 Ok(Self::Int(u))
327 } else {
328 let mut bytes = AccessionByteCode7::default();
329 for (byte_from, byte_to) in s.as_bytes().iter().rev().zip(bytes.iter_mut().rev()) {
330 *byte_to = *byte_from;
331 }
332 Ok(Self::Byte7(bytes))
333 }
334 }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
344#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
345#[cfg_attr(feature = "cv", derive(bincode::Encode, bincode::Decode))]
346pub struct CURIE {
347 pub controlled_vocabulary: ControlledVocabulary,
349 pub accession: AccessionIntCode,
351}
352
353impl CURIE {
354 pub const fn new(cv_id: ControlledVocabulary, accession: AccessionIntCode) -> Self {
360 Self {
361 controlled_vocabulary: cv_id,
362 accession,
363 }
364 }
365
366 pub fn as_param(&self) -> Param {
372 let mut param = Param::new();
373 param.controlled_vocabulary = Some(self.controlled_vocabulary);
374 param.accession = Some(self.accession);
375 param
376 }
377
378 #[inline(always)]
380 pub const fn accession_int(&self) -> u32 {
381 self.accession
382 }
383
384 #[inline(always)]
387 pub const fn controlled_vocabulary(&self) -> ControlledVocabulary {
388 self.controlled_vocabulary
389 }
390}
391
392impl Display for CURIE {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 write!(
395 f,
396 "{}:{:07}",
397 self.controlled_vocabulary.prefix(),
398 self.accession
399 )
400 }
401}
402
403impl<T: ParamLike> PartialEq<T> for CURIE {
404 fn eq(&self, other: &T) -> bool {
405 if !other.is_controlled()
406 || other
407 .controlled_vocabulary()
408 .map(|c| c != self.controlled_vocabulary)
409 .unwrap_or_default()
410 {
411 false
412 } else {
413 other
414 .accession()
415 .map(|a| a == self.accession)
416 .unwrap_or_default()
417 }
418 }
419}
420
421#[derive(Debug, Error)]
422pub enum CURIEParsingError {
423 #[error("{0} is not a recognized controlled vocabulary")]
426 UnknownControlledVocabulary(
427 #[from]
428 #[source]
429 ControlledVocabularyResolutionError,
430 ),
431 #[error("Failed to parse accession number {0}")]
435 AccessionParsingError(
436 #[from]
437 #[source]
438 num::ParseIntError,
439 ),
440 #[error("Did not detect a namespace separator ':' token")]
443 MissingNamespaceSeparator,
444}
445
446impl FromStr for CURIE {
447 type Err = CURIEParsingError;
448
449 fn from_str(s: &str) -> Result<Self, Self::Err> {
450 let mut tokens = s.split(':');
451 let cv = tokens
452 .next()
453 .ok_or(CURIEParsingError::MissingNamespaceSeparator)?;
454 let accession = tokens.next();
455 if accession.is_none() {
456 Err(CURIEParsingError::MissingNamespaceSeparator)
457 } else {
458 let cv: ControlledVocabulary = cv.parse::<ControlledVocabulary>()?;
459
460 let accession = accession.unwrap().parse()?;
461 Ok(CURIE::new(cv, accession))
462 }
463 }
464}
465
466impl TryFrom<&Param> for CURIE {
467 type Error = String;
468
469 fn try_from(value: &Param) -> Result<Self, Self::Error> {
470 match (value.controlled_vocabulary, value.accession) {
471 (Some(cv), Some(acc)) => Ok(CURIE::new(cv, acc)),
472 _ => Err(format!(
473 "{} is missing controlled vocabulary or accession",
474 value.name()
475 )),
476 }
477 }
478}
479
480impl<'a> TryFrom<&ParamCow<'a>> for CURIE {
481 type Error = String;
482
483 fn try_from(value: &ParamCow<'a>) -> Result<Self, Self::Error> {
484 match (value.controlled_vocabulary, value.accession) {
485 (Some(cv), Some(acc)) => Ok(CURIE::new(cv, acc)),
486 _ => Err(format!(
487 "{} is missing controlled vocabulary or accession",
488 value.name()
489 )),
490 }
491 }
492}
493
494pub fn curie_to_num(curie: &str) -> (Option<ControlledVocabulary>, Option<AccessionIntCode>) {
501 let mut parts = curie.split(':');
502 let prefix = match parts.next() {
503 Some(v) => v.parse::<ControlledVocabulary>().ok().and_then(|v| v.as_option()),
504 None => None,
505 };
506 if let Some(k) = parts.next() {
507 match k.parse() {
508 Ok(v) => (prefix, Some(v)),
509 Err(_) => (prefix, None),
510 }
511 } else {
512 (prefix, None)
513 }
514}