Skip to main content

mzdata_param/
lib.rs

1//! Elements of controlled vocabularies used to describe mass spectra and their components.
2//!
3//! This crate implements the "CV param" model used throughout the PSI-MS family of mass
4//! spectrometry file formats (mzML, imzML, mzMLb, ...): every piece of metadata is either
5//! a term drawn from a controlled vocabulary (like the PSI-MS ontology) or a free-text,
6//! user-defined key/value pair. Both are represented uniformly by [`Param`].
7//!
8//! ## Core types
9//!
10//! - [`Param`] / [`ParamCow`]: a single CV or user parameter. [`Param`] owns its data;
11//!   [`ParamCow`] borrows where possible (useful for `const` tables of well-known terms).
12//! - [`Value`] / [`ValueRef`]: the value half of a [`Param`], which may be a string,
13//!   integer, float, boolean, byte buffer, list, or empty. [`Value`] owns its data;
14//!   [`ValueRef`] borrows where possible. Both implement [`ParamValue`] for reading and
15//!   coercing the stored value.
16//! - [`CURIE`]: a `namespace:accession` identifier (e.g. `MS:1000041`) that names a term
17//!   within a [`ControlledVocabulary`].
18//! - [`ControlledVocabulary`]: the vocabulary a term belongs to (`MS`, `UO`, ...), and a
19//!   factory for building [`Param`]s within that namespace.
20//! - [`Unit`]: a closed set of units of measure (from the Unit Ontology and PSI-MS) that
21//!   a [`Param`]'s value may be expressed in.
22//! - [`ParamDescribed`] / [`ParamDescribedRead`]: traits for types that carry a list of
23//!   [`Param`]s (spectra, scans, instrument components, ...), with helpers to look a
24//!   parameter up by name, [`CURIE`], or accession string.
25//!
26//! ## Building a parameter
27//!
28//! Construct a controlled-vocabulary parameter from a [`ControlledVocabulary`] namespace:
29//!
30//! ```rust
31//! use mzdata_param::{ControlledVocabulary, ParamLike, ParamValue};
32//!
33//! let param = ControlledVocabulary::MS.param_val("MS:1000041", "charge state", 2i32);
34//! assert_eq!(param.name(), "charge state");
35//! assert_eq!(param.curie().unwrap().to_string(), "MS:1000041");
36//! assert_eq!(param.value().to_i64().unwrap(), 2);
37//! ```
38//!
39//! Or use [`ParamBuilder`] / [`Param::builder`] for more incremental construction,
40//! including plain user-defined parameters that have no controlled vocabulary:
41//!
42//! ```rust
43//! use mzdata_param::{Param, ParamValue, Unit, curie};
44//!
45//! let p = Param::builder()
46//!     .name("scan start time")
47//!     .curie(curie!(MS:1000016))
48//!     .value(12.34)
49//!     .unit(Unit::Minute)
50//!     .build();
51//! assert_eq!(p.to_f64().unwrap(), 12.34);
52//! assert_eq!(p.unit, Unit::Minute);
53//!
54//! // A user-defined parameter has no CURIE at all.
55//! let custom = Param::new_key_value("my custom field", "some value");
56//! assert!(!custom.is_controlled());
57//! ```
58//!
59//! ## Reading values generically
60//!
61//! [`ParamValue`] lets you read a [`Param`]'s value without caring whether it was parsed
62//! as a string, integer, or float - coercions are attempted on demand:
63//!
64//! ```rust
65//! use mzdata_param::{ControlledVocabulary, ParamValue};
66//!
67//! let p = ControlledVocabulary::MS.param_val("MS:1000827", "isolation window target m/z", "500.25");
68//! assert_eq!(p.to_f64().unwrap(), 500.25);
69//! assert_eq!(p.to_str(), "500.25");
70//! ```
71//!
72//! ## Attaching parameters to types with [`ParamDescribed`]
73//!
74//! The [`ParamDescribed`] trait provides many methods that make it easier to operate
75//! on instances of types which are described by a list of [`Param`]s.
76//!
77//! ### Simple implementation helpers
78//!
79//! Use [`impl_param_described!`] (for a plain `Vec<Param>` field) or
80//! [`impl_param_described_deferred!`] (for an `Option<Vec<Param>>` field that is
81//! lazily allocated on first write) to implement [`ParamDescribed`] for your type:
82//!
83//! ```rust
84//! use mzdata_param::{impl_param_described, Param, ParamDescribed, ParamList, ParamValue};
85//!
86//! #[derive(Default)]
87//! struct MyComponent {
88//!     params: ParamList,
89//! }
90//!
91//! impl_param_described!(MyComponent);
92//!
93//! let mut c = MyComponent::default();
94//! c.add_param(Param::new_key_value("vendor", "Acme"));
95//! assert_eq!(c.get_param_by_name("vendor").unwrap().to_str(), "Acme");
96//! ```
97//!
98//! ## Matching against known terms with `CURIE`
99//!
100//! [`CURIE`] implements `PartialEq` against anything implementing [`ParamLike`], so you can
101//! compare a parameter directly against a well-known accession, and the [`curie!`] macro
102//! gives a compact way to write one inline:
103//!
104//! ```rust
105//! use mzdata_param::{ControlledVocabulary, ParamLike, curie};
106//!
107//! let p = ControlledVocabulary::MS.param("MS:1000016", "scan start time");
108//! assert!(curie!(MS:1000016) == p);
109//! ```
110//!
111//! ## The PSI-MS ontology (`cv` feature)
112//!
113//! With the `cv` feature enabled, [`MSVocabulary`] gives access to the full PSI-MS
114//! ontology (term names, synonyms, and the `is_a` parent/child hierarchy), backed by an
115//! embedded static snapshot and an optional on-disk cache that can be refreshed from a
116//! `.obo` file via [`MSVocabulary::update_from_obo`]:
117//!
118//! ```rust
119//! # #[cfg(feature = "cv")]
120//! # {
121//! use mzdata_param::{MSVocabulary, curie};
122//!
123//! let term = MSVocabulary::get(curie!(MS:1000044)).unwrap();
124//! assert_eq!(term.name.as_ref(), "dissociation method");
125//! assert!(MSVocabulary::is_child_of(curie!(MS:1000133), curie!(MS:1000044)));
126//! # }
127//! ```
128use std::borrow::Cow;
129use std::convert::TryFrom;
130use std::fmt::Display;
131use std::hash::Hash;
132use std::num;
133use std::str::{self, FromStr};
134
135use thiserror::Error;
136
137pub(crate) mod value;
138
139pub use value::{ParamValue, ParamValueParseError, Value};
140
141pub(crate) mod value_ref;
142
143pub use value_ref::ValueRef;
144
145pub(crate) mod curie_;
146
147pub use curie_::{
148    AccessionCodeParseError, AccessionIntCode, CURIE, CURIEParsingError, ControlledVocabulary,
149    ControlledVocabularyResolutionError, curie_to_num,
150};
151
152#[cfg(feature = "cv")]
153pub(crate) mod cv;
154
155#[cfg(feature = "cv")]
156pub use cv::{CVTraversal, MSTerm, MSVocabulary, VocabularyData};
157
158/// A helper to generate methods that find a value by a [`CURIE`]
159#[macro_export]
160macro_rules! find_param_method {
161    ($meth:ident, $curie:expr) => {
162        $crate::find_param_method!($meth, $curie, "Find a parameter by its CURIE");
163    };
164    ($meth:ident, $curie:expr, $desc:literal) => {
165        #[doc=$desc]
166        pub fn $meth(&self) -> Option<$crate::ValueRef<'_>> {
167            self.get_param_by_curie($curie)
168                .map(|p| $crate::ParamLike::value(p))
169        }
170    };
171    ($meth:ident, $curie:expr, $conv:expr, $result:ty) => {
172        $crate::find_param_method!(
173            $meth,
174            $curie,
175            $conv,
176            $result,
177            "Find a parameter by its CURIE"
178        );
179    };
180    ($meth:ident, $curie:expr, $conv:expr, $result:ty, $desc:literal) => {
181        #[doc=$desc]
182        pub fn $meth(&self) -> $result {
183            self.get_param_by_curie($curie).map($conv)
184        }
185    };
186}
187
188/// A syntactic shortcut for creating [`CURIE`] instances using compact notation.
189///
190/// The following are identical.
191/// ```rust
192/// # use mzdata_param::{CURIE, ControlledVocabulary, curie};
193/// assert_eq!(CURIE::new(ControlledVocabulary::MS, 1000016), curie!(MS:1000016));
194/// ```
195///
196/// This macro has two advantages for writing [`CURIE`] "constants", it does not
197/// require a function call (so may be used in `match` without extra overhead) and
198/// is far more succinct.
199#[macro_export]
200macro_rules! curie {
201    ($ns:ident:$acc:literal) => {
202        $crate::CURIE {
203            controlled_vocabulary: $crate::ControlledVocabulary::$ns,
204            accession: $acc,
205        }
206    };
207}
208
209/// A minimal, read-only view over a single CV or user-defined parameter.
210///
211/// Both [`Param`] and [`ParamCow`] implement this trait; prefer it when writing code that
212/// should work with either the owned or borrowed representation.
213pub trait ParamLike {
214    /// The human-readable name of the parameter.
215    fn name(&self) -> &str;
216    /// A borrowed view of the parameter's value.
217    fn value(&self) -> ValueRef<'_>;
218    /// The numeric accession code within [`ParamLike::controlled_vocabulary`], if this
219    /// parameter is drawn from a controlled vocabulary.
220    fn accession(&self) -> Option<AccessionIntCode>;
221    /// The controlled vocabulary this parameter's term belongs to, if any.
222    fn controlled_vocabulary(&self) -> Option<ControlledVocabulary>;
223    /// The unit the parameter's value is expressed in, if known.
224    fn unit(&self) -> Unit;
225
226    /// Check whether this parameter's term belongs to the PSI-MS controlled vocabulary.
227    fn is_ms(&self) -> bool {
228        if let Some(cv) = self.controlled_vocabulary() {
229            cv == ControlledVocabulary::MS
230        } else {
231            false
232        }
233    }
234
235    /// Parse the parameter's value as `T`. See [`ParamValue::parse`].
236    fn parse<T: str::FromStr>(&self) -> Result<T, T::Err> {
237        self.value().parse::<T>()
238    }
239
240    /// Check whether this parameter is drawn from a controlled vocabulary, as opposed to
241    /// being a free-text user-defined parameter.
242    fn is_controlled(&self) -> bool {
243        self.accession().is_some()
244    }
245
246    /// Build a [`CURIE`] from [`ParamLike::controlled_vocabulary`] and
247    /// [`ParamLike::accession`], if both are present.
248    fn curie(&self) -> Option<CURIE> {
249        if !self.is_controlled() {
250            None
251        } else {
252            let cv = self.controlled_vocabulary().unwrap();
253            let acc = self.accession().unwrap();
254            // let accession_str = format!("{}:{:07}", cv.prefix(), acc);
255            Some(CURIE::new(cv, acc))
256        }
257    }
258}
259
260pub(crate) mod param_cow;
261pub use param_cow::ParamCow;
262
263pub(crate) mod param;
264
265pub use param::{Param, ParamBuilder};
266
267/// Anything that can be converted into an accession code portion of a [`CURIE`]
268#[derive(Debug, Clone)]
269pub enum AccessionLike<'a> {
270    Text(Cow<'a, str>),
271    Number(AccessionIntCode),
272    CURIE(CURIE),
273}
274
275impl From<AccessionIntCode> for AccessionLike<'_> {
276    fn from(value: AccessionIntCode) -> Self {
277        Self::Number(value)
278    }
279}
280
281impl<'a> From<&'a str> for AccessionLike<'a> {
282    fn from(value: &'a str) -> Self {
283        Self::Text(Cow::Borrowed(value))
284    }
285}
286
287impl From<String> for AccessionLike<'_> {
288    fn from(value: String) -> Self {
289        Self::Text(Cow::Owned(value))
290    }
291}
292
293/// The concrete container type used to hold a [`Param`] collection, as referenced by
294/// [`ParamDescribed`] and the [`impl_param_described!`]/[`impl_param_described_deferred!`]
295/// macros.
296pub type ParamList = Vec<Param>;
297
298/// A read-only form of [`ParamDescribed`], implemented directly for `&[Param]` so that a
299/// borrowed slice of parameters can be queried the same way as a type that owns its
300/// parameter list.
301pub trait ParamDescribedRead {
302    /// Obtain an immutable slice over the encapsulated [`Param`] list
303    fn params(&self) -> &[Param];
304
305    /// Find the first [`Param`] whose name matches `name`
306    fn get_param_by_name(&self, name: &str) -> Option<&Param> {
307        self.params().iter().find(|&param| param.name == name)
308    }
309
310    /// Find the first [`Param`] whose [`CURIE`] matches `curie`
311    fn get_param_by_curie(&self, curie: &CURIE) -> Option<&Param> {
312        self.params().iter().find(|&param| curie == param)
313    }
314
315    /// Find the first [`Param`] whose [`Param::accession`] matches `accession`
316    ///
317    /// This is equivalent to [`ParamDescribed::get_param_by_curie`] on `accession.parse::<CURIE>().unwrap()`
318    fn get_param_by_accession(&self, accession: &str) -> Option<&Param> {
319        let (cv, acc_num) = curie_to_num(accession);
320        self.params()
321            .iter()
322            .find(|&param| param.accession == acc_num && param.controlled_vocabulary == cv)
323    }
324
325    /// Iterate over the encapsulated parameter list
326    fn iter_params(&self) -> std::slice::Iter<'_, Param> {
327        self.params().iter()
328    }
329}
330
331impl ParamDescribedRead for &[Param] {
332    fn params(&self) -> &[Param] {
333        self
334    }
335}
336
337
338/// A type that has a [`ParamList`] that uses [`Param`] instances to describe an entity
339/// with key-value pairs.
340///
341/// Most implementors get this via [`impl_param_described!`] or
342/// [`impl_param_described_deferred!`] rather than implementing it by hand.
343pub trait ParamDescribed {
344    /// Obtain an immutable slice over the encapsulated [`Param`] list
345    fn params(&self) -> &[Param];
346
347    /// Obtain an mutable slice over the encapsulated [`Param`] list
348    fn params_mut(&mut self) -> &mut ParamList;
349
350    /// Add a new [`Param`] to the entity
351    fn add_param(&mut self, param: Param) {
352        self.params_mut().push(param);
353    }
354
355    /// Add all parameters from an iterator of [`Param`] to the entity
356    fn extend_params(&mut self, it: impl IntoIterator<Item = Param>) {
357        self.params_mut().extend(it)
358    }
359
360    /// Remove the `i`th [`Param`] from the entity.
361    fn remove_param(&mut self, index: usize) -> Param {
362        self.params_mut().remove(index)
363    }
364
365    /// Find the first [`Param`] whose name matches `name`
366    fn get_param_by_name(&self, name: &str) -> Option<&Param> {
367        self.params().iter().find(|&param| param.name == name)
368    }
369
370    /// Find the first [`Param`] whose [`CURIE`] matches `curie`
371    fn get_param_by_curie(&self, curie: &CURIE) -> Option<&Param> {
372        self.params().iter().find(|&param| curie == param)
373    }
374
375    /// Find the first [`Param`] whose [`Param::accession`] matches `accession`
376    ///
377    /// This is equivalent to [`ParamDescribed::get_param_by_curie`] on `accession.parse::<CURIE>().unwrap()`
378    fn get_param_by_accession(&self, accession: &str) -> Option<&Param> {
379        let (cv, acc_num) = curie_to_num(accession);
380        self.params()
381            .iter()
382            .find(|&param| param.accession == acc_num && param.controlled_vocabulary == cv)
383    }
384
385    /// Iterate over the encapsulated parameter list
386    fn iter_params(&self) -> std::slice::Iter<'_, Param> {
387        self.params().iter()
388    }
389
390    /// Iterate mutably over the encapsulated parameter list
391    fn iter_params_mut(&mut self) -> std::slice::IterMut<'_, Param> {
392        self.params_mut().iter_mut()
393    }
394}
395
396impl ParamDescribed for ParamList {
397    fn params(&self) -> &[Param] {
398        self
399    }
400
401    fn params_mut(&mut self) -> &mut ParamList {
402        self
403    }
404}
405
406
407/// Implement the [`ParamDescribed`] trait for type `$t`, referencing a `params` member
408/// of type `Vec<`[`Param`]`>`.
409#[macro_export]
410macro_rules! impl_param_described {
411    ($($t:ty), +) => {$(
412
413        impl $crate::ParamDescribed for $t {
414            fn params(&self) -> &[$crate::Param] {
415                return &self.params
416            }
417
418            fn params_mut(&mut self) -> &mut $crate::ParamList {
419                return &mut self.params
420            }
421        }
422    )+};
423}
424
425#[doc(hidden)]
426pub const _EMPTY_PARAM: &[Param] = &[];
427
428/// Implement the [`ParamDescribed`] trait for type `$t`, referencing a `params` member
429/// that is an `Option<Vec<`[`Param`]`>>` that will lazily be initialized automatically
430/// when it is accessed mutably.
431#[macro_export]
432macro_rules! impl_param_described_deferred {
433    ($($t:ty), +) => {$(
434        impl $crate::ParamDescribed for $t {
435            fn params(&self) -> &[$crate::Param] {
436                match &self.params {
437                    Some(val) => &val,
438                    None => {
439                        $crate::_EMPTY_PARAM
440                    }
441                }
442            }
443
444            fn params_mut(&mut self) -> &mut $crate::ParamList {
445                let val = &mut self.params;
446                if val.is_some() {
447                    return val.as_deref_mut().unwrap()
448                } else {
449                    *val = Some(Box::default());
450                    return val.as_deref_mut().unwrap()
451                }
452            }
453        }
454    )+};
455}
456
457pub(crate) mod units;
458
459pub use units::Unit;
460
461#[cfg(test)]
462mod test {
463    use super::*;
464
465    #[test]
466    fn test_build_param() {
467        assert_eq!(
468            ParamBuilder::default()
469                .name("dalton")
470                .curie(curie!(UO:221))
471                .build(),
472            ControlledVocabulary::UO.param("UO:000221", "dalton")
473        );
474        // <cvParam cvRef="MS" accession="MS:1000529" name="instrument serial number" value="FSN10375"/>
475        let p = ParamBuilder::default()
476            .controlled_vocabulary(ControlledVocabulary::MS)
477            .accession(1000529)
478            .name("instrument serial number")
479            .value("FSN10375")
480            .unit(Unit::Unknown)
481            .build();
482        assert_eq!(p.value(), "FSN10375");
483        assert_eq!(p.unit(), Unit::Unknown);
484    }
485
486    #[test]
487    fn test_value() {
488        let x = 42;
489        let mut val: Value = x.into();
490        let mut val_ref: ValueRef = x.into();
491        let mut val_ref2: ValueRef = (&x).into();
492        assert_eq!(val, x);
493        assert_eq!(val_ref, x);
494        assert_eq!(val_ref, val);
495        assert_eq!(val_ref2, val_ref);
496        assert!(val.to_bool().unwrap());
497        assert!(val_ref.to_bool().unwrap());
498        assert_eq!(val.to_str(), x.to_string());
499        assert_eq!(val_ref.to_str(), x.to_string());
500        val = x.to_string().parse().unwrap();
501        val_ref = x.to_string().parse().unwrap();
502        assert_eq!(val, x);
503        assert_eq!(val_ref, x);
504        assert_eq!(val_ref, val);
505
506        let x2 = Some(x);
507        val = x2.into();
508        val_ref = x2.into();
509        val_ref2 = (&x).into();
510        assert_eq!(val, x);
511        assert_eq!(val_ref, x);
512        assert_eq!(val_ref, val);
513        assert_eq!(val_ref2, val_ref);
514        assert!(val.to_bool().unwrap());
515        assert!(val_ref.to_bool().unwrap());
516        assert_eq!(val.to_str(), x.to_string());
517        assert_eq!(val_ref.to_str(), x.to_string());
518        val = x.to_string().parse().unwrap();
519        val_ref = x.to_string().parse().unwrap();
520        assert_eq!(val, x);
521        assert_eq!(val_ref, x);
522        assert_eq!(val_ref, val);
523
524        let x = 42.01;
525        val = x.into();
526        val_ref = x.into();
527        val_ref2 = (&x).into();
528        assert_eq!(val, x);
529        assert_eq!(val_ref, x);
530        assert_eq!(val_ref, val);
531        assert_eq!(val_ref2, val_ref);
532        assert!(val.to_bool().unwrap());
533        assert!(val_ref.to_bool().unwrap());
534        assert_eq!(val.to_str(), x.to_string());
535        assert_eq!(val_ref.to_str(), x.to_string());
536        val = x.to_string().parse().unwrap();
537        val_ref = x.to_string().parse().unwrap();
538        assert_eq!(val, x);
539        assert_eq!(val_ref, x);
540        assert_eq!(val_ref, val);
541
542        let x2 = Some(x);
543        val = x2.into();
544        val_ref = x2.into();
545        assert_eq!(val, x);
546        assert_eq!(val_ref, x);
547        assert_eq!(val_ref, val);
548        assert!(val.to_bool().unwrap());
549        assert!(val_ref.to_bool().unwrap());
550        assert_eq!(val.to_str(), x.to_string());
551        assert_eq!(val_ref.to_str(), x.to_string());
552        val = x.to_string().parse().unwrap();
553        val_ref = x.to_string().parse().unwrap();
554        assert_eq!(val, x);
555        assert_eq!(val_ref, x);
556        assert_eq!(val_ref, val);
557
558        let x = true;
559        val = x.into();
560        val_ref = x.into();
561        assert_eq!(val, x);
562        assert_eq!(val_ref, x);
563        assert_eq!(val_ref, val);
564        assert!(val.to_bool().unwrap());
565        assert!(val_ref.to_bool().unwrap());
566        assert_eq!(val.to_str(), x.to_string());
567        assert_eq!(val_ref.to_str(), x.to_string());
568        val = x.to_string().parse().unwrap();
569        val_ref = x.to_string().parse().unwrap();
570        assert_eq!(val, x);
571        assert_eq!(val_ref, x);
572        assert_eq!(val_ref, val);
573
574        let x = "Foobar".to_string();
575        val = x.clone().into();
576        val_ref = x.clone().into();
577        val_ref2 = x.as_str().into();
578        assert_eq!(val, x);
579        assert_eq!(val_ref, x);
580        assert_eq!(val_ref, val);
581        assert_eq!(val_ref2, val_ref);
582        assert_eq!(val.to_str(), x.to_string());
583        assert_eq!(val_ref.to_str(), x.to_string());
584        val = x.to_string().parse().unwrap();
585        val_ref = x.to_string().parse().unwrap();
586        assert_eq!(val, x);
587        assert_eq!(val_ref, x);
588        assert_eq!(val_ref, val);
589        assert_eq!(val.to_buffer().unwrap(), x.as_bytes());
590        assert_eq!(val_ref.to_buffer().unwrap(), x.as_bytes());
591        assert_eq!(val_ref.to_buffer().unwrap(), val.to_buffer().unwrap());
592    }
593
594    #[cfg(feature = "static_data")]
595    #[test]
596    fn test_mzcv_ms() {
597        let cv = MSVocabulary::init();
598        let term = cv.get_by_name("ms level").unwrap();
599        assert_eq!(term.name.as_ref(), "ms level");
600        let parents_of = CVTraversal::parents_of(cv, term.curie()).unwrap();
601        let parent = cv
602            .get_by_index(&parents_of.iter().next().unwrap().0)
603            .unwrap();
604        assert_eq!(parent.name.as_ref(), "spectrum attribute");
605    }
606}