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    /// A sentinel for a namespace that could not be recognized, e.g. while parsing an
31    /// unfamiliar CURIE prefix. Not a valid namespace to build a [`Param`] or [`CURIE`]
32    /// from - [`ControlledVocabulary::prefix`] and [`ControlledVocabulary::as_bytes`]
33    /// panic on it.
34    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    /// Get the CURIE namespace prefix for this controlled vocabulary
86    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    /// Like [`ControlledVocabulary::prefix`], but obtain a byte string instead
104    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    /// Convert [`ControlledVocabulary::Unknown`] to `None`, and any other variant to
122    /// `Some(self)`
123    pub const fn as_option(&self) -> Option<Self> {
124        match self {
125            Self::Unknown => None,
126            _ => Some(*self),
127        }
128    }
129
130    /// Create a [`Param`] whose accession comes from this controlled vocabulary namespace with
131    /// an empty value.
132    ///
133    /// # Arguments
134    /// - `accession`: The accession code for the [`Param`]. If specified as a [`CURIE`] or a string-like type,
135    ///   any namespace is ignored.
136    /// - `name`: The name of the parameter
137    /// # See Also
138    /// - [`ControlledVocabulary::param_val`]
139    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    /// Build a [`CURIE`] within this namespace from an accession code.
165    pub const fn curie(&self, accession: AccessionIntCode) -> CURIE {
166        CURIE::new(*self, accession)
167    }
168
169    /// Create a [`ParamCow`] from this namespace in a `const` context, useful for preparing
170    /// global constants or inlined variables.
171    ///
172    /// All parameters must have a `'static` lifetime.
173    ///
174    /// # Arguments
175    /// - `name`: The name of the controlled vocabulary term.
176    /// - `value`: The wrapped value as a constant.
177    /// - `accession`: The a priori determined accession code for the term
178    /// - `unit`: The unit associated with the value
179    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    /// Create a [`ParamCow`] from this namespace in a `const` context with an empty
196    /// value and no unit.
197    ///
198    /// See [`ControlledVocabulary::const_param`] for more details.
199    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    /// Create a [`ParamCow`] from this namespace in a `const` context with an empty
208    /// value but a specified unit.
209    ///
210    /// This is intended to create a "template" that will be copied and have a value specified.
211    ///
212    /// See [`ControlledVocabulary::const_param`] for more details.
213    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    /// Create a [`Param`] whose accession comes from this controlled vocabulary namespace with
223    /// the given value.
224    ///
225    /// # Arguments
226    /// - `accession`: The accession code for the [`Param`]. If specified as a [`CURIE`] or a string-like type,
227    ///   any namespace is ignored.
228    /// - `name`: The name of the parameter
229    /// - `value`: The value of the parameter
230    ///
231    /// # See Also
232    /// - [`ControlledVocabulary::param`]
233    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/// An error describing a failure to map a controlled vocabulary identifier
246/// to a known namespace
247#[derive(Debug, Clone, Error)]
248pub enum ControlledVocabularyResolutionError {
249    /// The namespace prefix (e.g. `"MS"`) was not one of the recognized [`ControlledVocabulary`] values.
250    #[error("Unrecognized controlled vocabulary {0}")]
251    UnknownControlledVocabulary(String),
252    /// The `u8` discriminant did not correspond to any [`ControlledVocabulary`] variant.
253    /// This value is unstable.
254    #[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
276/// The integer type used to store a [`CURIE`]'s accession number.
277pub type AccessionIntCode = u32;
278/// A fixed-width byte representation of a 7-digit accession code. A future
279/// CURIE implementation might use this for non-numeric accessions.
280pub 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/// A CURIE is a namespace + accession identifier, of the form `<namespace>:<identifier>`.
338///
339/// A CURIE is a "compact URI": <https://www.w3.org/TR/curie/>. This implementation assumes
340/// that that the accession code is a number *and* that an accession code is always present,
341/// which is not universally true. It is however sufficient for most use-cases in this
342/// library's domain.
343#[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    /// The controlled vocabulary namespace, e.g. `MS`.
348    pub controlled_vocabulary: ControlledVocabulary,
349    /// The numeric accession code within the namespace, e.g. `1000016`.
350    pub accession: AccessionIntCode,
351}
352
353impl CURIE {
354    /// Build a [`CURIE`] from a namespace and accession code directly.
355    ///
356    /// This is a `const` function, but it cannot be used in a context that does
357    /// not allow function calls. Prefer the [`curie`] macro when writing "constant"
358    /// [`CURIE`]s
359    pub const fn new(cv_id: ControlledVocabulary, accession: AccessionIntCode) -> Self {
360        Self {
361            controlled_vocabulary: cv_id,
362            accession,
363        }
364    }
365
366    /// Create an otherwise-empty [`Param`] carrying this [`CURIE`]'s namespace and
367    /// accession, with an empty name and value.
368    ///
369    /// This is technically valid, but most users will also expect [`Param::name`] to
370    /// be set.
371    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    /// The numeric accession code, equivalent to reading [`CURIE::accession`] directly.
379    #[inline(always)]
380    pub const fn accession_int(&self) -> u32 {
381        self.accession
382    }
383
384    /// The controlled vocabulary namespace, equivalent to reading
385    /// [`CURIE::controlled_vocabulary`] directly.
386    #[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    /// Indicates that the controlled vocabulary wasn't recognized, even
424    /// if it is perfectly valid, we just don't cover it.
425    #[error("{0} is not a recognized controlled vocabulary")]
426    UnknownControlledVocabulary(
427        #[from]
428        #[source]
429        ControlledVocabularyResolutionError,
430    ),
431    /// Indicates that the accession code wasn't successfully parsed into
432    /// an integer. Non-numeric accession codes are not supported by this
433    /// library.
434    #[error("Failed to parse accession number {0}")]
435    AccessionParsingError(
436        #[from]
437        #[source]
438        num::ParseIntError,
439    ),
440    /// Indicates that a ":" wasn't detected, which indicates a malformed
441    /// CURIE anywhere.
442    #[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
494/// Split a CURIE-like string (`"MS:1000016"`) into its namespace and accession parts,
495/// without requiring either to be valid.
496///
497/// Unlike [`CURIE::from_str`], this never fails: an unrecognized namespace resolves to
498/// `None` rather than an error, and a missing or non-numeric accession likewise resolves
499/// to `None`.
500pub 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}