imzml 0.1.3

A library for reading the mass spectrometry (imaging) formats mzML and imzML.
Documentation
/// Parsing of .obo files
pub mod parser;

use std::ops::Deref;
use std::sync::Arc;

use chrono::{DateTime, FixedOffset, NaiveDateTime};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};

use crate::obo::parser::{parse_imzml_ontology, parse_mzml_ontology};

//pub type OBODictionary = std::collections::HashMap<Arc<str>, Arc<OBOTerm>>;

lazy_static! {
    /// imagingMS.obo
    static ref IMS_OBO: &'static str = include_str!("imagingMS.obo");
    /// psi-ms.obo
    static ref MS_OBO: &'static str = include_str!("psi-ms.obo");
    /// pato.obo
    static ref PATO_OBO: &'static str = include_str!("pato.obo");
    /// uo.obo
    static ref UO_OBO: &'static str = include_str!("uo.obo");
    /// Default mzML ontology (uses current version included in the project)
    pub static ref MZML_ONTOLOGY: Ontology = parse_mzml_ontology().unwrap();
    /// Default imzML ontology (uses current version included in the project)
    pub static ref IMZML_ONTOLOGY: Ontology = parse_imzml_ontology().unwrap();
}

// /// Ontology term
// #[derive(Debug)]
// pub struct OBOTerm {
//     /// Accession (unique identifier)
//     pub accession: String,
//     /// Descriptive name of the term
//     pub name: String,
// }

// impl OBOTerm {
//     /// Creates a new OBOTerm with the supplied accession (unique id) and descriptive name
//     pub fn new(accession: &str, name: &str) -> OBOTerm {
//         OBOTerm {
//             accession: accession.into(),
//             name: name.into(),
//         }
//     }

//     /// Returns
//     pub fn accession(&self) -> &str {
//         &self.accession
//     }

//     pub fn name(&self) -> &str {
//         &self.name
//     }
// }

// See: https://owlcollab.github.io/oboformat/doc/GO.format.obo-1_4.html
#[derive(Debug)]
struct OBOHeader {
    format_version: String,
    data_version: Option<String>,
    date: Option<NaiveDateTime>,
    saved_by: Option<String>,
    auto_generated_by: Option<String>,
    import: Option<Vec<String>>,
    subsetdef: Option<Vec<String>>,
    synonymtypedef: Option<Vec<String>>,
    default_namespace: Option<String>,
    namespace_id_rule: Option<Vec<String>>,
    idspace: Option<Vec<String>>,
    //treat-xrefs-as-equivalent
    //treat-xrefs-as-genus-differentia
    //treat-xrefs-as-relationship
    //treat-xrefs-as-is_a
    //default_relationship_id_prefix: Option<String>,
    //id_mapping: Option<Vec<String>>,
    remark: Option<Vec<String>>,
    //relax-unique-identifier-assumption-for-namespace
    //relax-unique-label-assumption-for-namespace
    ontology: Option<String>,
}

impl OBOHeader {
    fn new(format_version: &str) -> Self {
        OBOHeader {
            format_version: format_version.to_string(),
            data_version: None,
            ontology: None,
            date: None,
            saved_by: None,
            auto_generated_by: None,
            namespace_id_rule: None,
            subsetdef: None,
            import: None,
            synonymtypedef: None,
            idspace: None,
            remark: None,
            default_namespace: None,
        }
    }
}

/// Single controlled vocabulary term
#[derive(Debug, Clone)]
pub struct OBOTerm(Arc<Term>);

impl Deref for OBOTerm {
    type Target = Term;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Ontology.
/// Stores set of controlled vocabulary terms
#[derive(Debug, Clone)]
pub struct Ontology(Arc<OntologyInner>);

impl Deref for Ontology {
    type Target = OntologyInner;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Ontology is a set of terms descirbing something, with an accompanying unique identifier
#[derive(Debug)]
pub struct OntologyInner {
    header: OBOHeader,

    typedefs: Vec<Typedef>,
    term_dicts: HashMap<String, HashMap<String, OBOTerm>>,

    imports: Vec<Ontology>,
}

impl OntologyInner {
    /// Returns the format version (included in the header)
    pub fn format_version(&self) -> &str {
        &self.header.format_version
    }

    pub fn data_version(&self) -> Option<&str> {
        self.header.data_version.as_deref()
    }

    /// Type definitions included in the header
    pub fn typedefs(&self) -> &[Typedef] {
        &self.typedefs
    }

    /// Synonym type definitions
    pub fn synonymtypedefs(&self) -> Option<&Vec<String>> {
        self.header.synonymtypedef.as_ref()
    }

    /// idspaces
    pub fn idspace(&self) -> Option<&Vec<String>> {
        self.header.idspace.as_ref()
    }

    /// Returns hashmap of all terms within the specified namespace
    #[inline]
    pub fn namespace_term_map(&self, namespace: &str) -> Option<&HashMap<String, OBOTerm>> {
        self.term_dicts.get(namespace)
    }

    /// Returns a Term with the give id if one exists, or None otherwise.
    #[inline]
    pub fn get(&self, id: &str) -> Option<&OBOTerm> {
        let mut split = id.split(':');

        if let Some(namespace) = split.next() {
            // Check namespace
            if let Some(term_dict) = self.namespace_term_map(namespace) {
                return term_dict.get(id);
            } else {
                for import in &self.imports {
                    if let Some(term) = import.get(id) {
                        return Some(term);
                    }
                }
            }
        }

        None
    }

    // #[inline]
    // pub fn contains_key(&self, id: &str) -> bool {
    //     match self.terms.contains_key(id) {
    //         true => true,
    //         false => {
    //             for import in &self.imports {
    //                 if import.contains_key(id) {
    //                     return true;
    //                 }
    //             }

    //             false
    //         }
    //     }
    // }
}

/// Describes a type definition
#[derive(Debug)]
pub struct Typedef {
    id: String,
    name: Option<String>,
    namespace: Option<String>,
    synonym: Option<Vec<String>>,
    is_transitive: Option<bool>,
    def: Option<String>,
    comment: Option<String>,
    domain: Option<String>,
    xref: Option<String>,
    is_a: Option<Vec<String>>,
    range: Option<String>,
    created_by: Option<String>,
    creation_date: Option<DateTime<FixedOffset>>,
    is_metadata_tag: bool,
    is_class_level: bool,
}

impl Typedef {
    fn new(id: &str) -> Self {
        Typedef {
            id: id.to_string(),
            name: None,
            namespace: None,
            synonym: None,
            is_transitive: None,
            def: None,
            comment: None,
            domain: None,
            xref: None,
            is_a: None,
            range: None,
            created_by: None,
            creation_date: None,
            is_metadata_tag: false,
            is_class_level: false,
        }
    }

    /// Returns the unique identifier for the type definition.
    pub fn id(&self) -> &str {
        &self.id
    }
}

/// Describes an ontology term
#[derive(Debug)]
pub struct Term {
    id: String,
    alt_id: Option<Vec<String>>,
    name: Option<String>,
    def: Option<String>,
    xref: Option<String>,
    namespace: Option<String>,
    consider: Option<Vec<String>>,
    is_a: Option<Vec<String>>,
    created_by: Option<String>,
    creation_date: Option<DateTime<FixedOffset>>,
    relationship: Option<Vec<String>>,
    replaced_by: Option<Vec<String>>,
    property_value: Option<Vec<String>>,
    subset: Option<Vec<String>>,
    intersection_of: Option<Vec<String>>,
    disjoint_from: Option<Vec<String>>,
    synonym: Option<Vec<String>>,
    comment: Option<String>,
    value_type: Option<ValueType>,
    is_obsolete: bool,
    //    children: Vec<Weak<Term>>,
    //    parents: Vec<Weak<Term>>,
}

impl Term {
    fn new(id: &str) -> Self {
        Term {
            id: id.to_string(),
            alt_id: None,
            name: None,
            def: None,
            xref: None,
            namespace: None,
            consider: None,
            is_a: None,
            created_by: None,
            creation_date: None,
            relationship: None,
            replaced_by: None,
            property_value: None,
            subset: None,
            intersection_of: None,
            disjoint_from: None,
            synonym: None,
            comment: None,
            value_type: None,
            is_obsolete: false,
        }
    }

    /// Returns the unique identifier for the term
    #[inline]
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Returns the namespace to which the term belongs.
    #[inline]
    pub fn namespace(&self) -> Option<&str> {
        self.id.split(':').next()
    }

    /// Returns the name describing the term.
    #[inline]
    pub fn name(&self) -> Option<&str> {
        self.name.as_deref()
    }

    /// Retruns the expected value type for the term, if one exists.
    #[inline]
    pub fn value_type(&self) -> Option<ValueType> {
        self.value_type
    }

    /// Returns true if the term is a child term (using the ontology is_a hierarchy) of the term with the supplied `parent_id`
    pub fn is_child_of(&self, ontology: &Ontology, parent_id: &str) -> bool {
        // TODO: Maybe there is a better way to do this, but for now passing in the ontology seems to
        // avoid a lot of issues with Rc and RefCell

        if let Some(is_a) = &self.is_a {
            for current_term in is_a {
                if parent_id == current_term {
                    return true;
                }

                // Check parent for grandparent
                if let Some(parent) = ontology.get(current_term) {
                    if parent.is_child_of(ontology, parent_id) {
                        return true;
                    }
                }
            }
        }

        false
    }
}

/// Possible types for values included alongside ontology terms.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ValueType {
    /// Boolean (true/false)
    Boolean,
    /// A signed 8-bit integer
    Byte,
    /// A decimal value
    Decimal,
    /// A signed 32-bit integer
    Int,
    /// An integer value
    Integer,
    /// A signed 64-bit integer
    Long,
    /// An integer containing only negative values (..,-2,-1)
    NegativeInteger,
    /// An integer containing only negative values (0, 1, 2, ..)
    NonNegativeInteger,
    /// Positive integer (same as above?)
    PositiveInteger,

    /// This is not a built in type (<https://www.w3.org/TR/xmlschema-2/>) but appears in the imagingMS.obo
    NonNegativeFloat,

    /// Floating point value
    Float,
    /// Double precision value
    Double,
    /// String of characters
    String,
    /// Date and time
    DateTime,
    /// URI
    AnyURI,
}