Skip to main content

mzdata_param/
curie_.rs

1use super::*;
2
3/// Controlled vocabularies used in mass spectrometry data files
4#[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    /// The PSI-MS Controlled Vocabulary [https://www.ebi.ac.uk/ols4/ontologies/ms](https://www.ebi.ac.uk/ols4/ontologies/ms)
10    MS = 1,
11    /// The Unit Ontology [https://www.ebi.ac.uk/ols4/ontologies/uo](https://www.ebi.ac.uk/ols4/ontologies/uo)
12    UO,
13    /// The Experimental Factor Ontology <https://www.ebi.ac.uk/ols4/ontologies/efo>
14    EFO,
15    /// The Ontology for Biomedical Investigations <https://www.ebi.ac.uk/ols4/ontologies/obi>
16    OBI,
17    /// The Human Ancestry Ontology <https://www.ebi.ac.uk/ols4/ontologies/hancestro>
18    HANCESTRO,
19    /// The Basic Formal Ontology <https://www.ebi.ac.uk/ols4/ontologies/bfo>
20    BFO,
21    /// The NCI Thesaurus OBO Edition <https://www.ebi.ac.uk/ols4/ontologies/ncit>
22    NCIT,
23    /// The BRENDA Tissue Ontology <https://www.ebi.ac.uk/ols4/ontologies/bto>
24    BTO,
25    /// The PRIDE Controlled Vocabulary <https://www.ebi.ac.uk/ols4/ontologies/pride>
26    PRIDE,
27    /// The Imaging MS Controlled Vocabulary <https://www.ms-imaging.org/imzml/>
28    #[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    /// Get the CURIE namespace prefix for this controlled vocabulary
82    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    /// Like [`ControlledVocabulary::prefix`], but obtain a byte string instead
100    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    /// Create a [`Param`] whose accession comes from this controlled vocabulary namespace with
125    /// an empty value.
126    ///
127    /// # Arguments
128    /// - `accession`: The accession code for the [`Param`]. If specified as a [`CURIE`] or a string-like type,
129    ///   any namespace is ignored.
130    /// - `name`: The name of the parameter
131    /// # See Also
132    /// - [`ControlledVocabulary::param_val`]
133    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    /// Create a [`ParamCow`] from this namespace in a `const` context, useful for preparing
163    /// global constants or inlined variables.
164    ///
165    /// All parameters must have a `'static` lifetime.
166    ///
167    /// # Arguments
168    /// - `name`: The name of the controlled vocabulary term.
169    /// - `value`: The wrapped value as a constant.
170    /// - `accession`: The a priori determined accession code for the term
171    /// - `unit`: The unit associated with the value
172    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    /// Create a [`ParamCow`] from this namespace in a `const` context with an empty
189    /// value and no unit.
190    ///
191    /// See [`ControlledVocabulary::const_param`] for more details.
192    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    /// Create a [`ParamCow`] from this namespace in a `const` context with an empty
201    /// value but a specified unit.
202    ///
203    /// This is intended to create a "template" that will be copied and have a value specified.
204    ///
205    /// See [`ControlledVocabulary::const_param`] for more details.
206    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    /// Create a [`Param`] whose accession comes from this controlled vocabulary namespace with
216    /// the given value.
217    ///
218    /// # Arguments
219    /// - `accession`: The accession code for the [`Param`]. If specified as a [`CURIE`] or a string-like type,
220    ///   any namespace is ignored.
221    /// - `name`: The name of the parameter
222    /// - `value`: The value of the parameter
223    ///
224    /// # See Also
225    /// - [`ControlledVocabulary::param`]
226    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/// An error describing a failure to map a controlled vocabulary identifier
239/// to a known namespace
240#[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/// A CURIE is a namespace + accession identifier
325#[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}