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(|¶m| 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(|¶m| 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(|¶m| 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(|¶m| 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(|¶m| 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(|¶m| 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}