mzident 0.2.1

Handle all kinds of PSM files.
Documentation
use std::path::Path;

use context_error::*;
use mzcore::{
    ontology::Ontologies,
    sequence::{Linked, SemiAmbiguous, SimpleLinear},
};
use mzcv::Term;
use serde::{Deserialize, Serialize};

use crate::*;

/// Open the selected path and automatically determine the filetype. It will decompress gzipped
/// files automatically.
///
/// # Errors
/// It errors if the filetype could not be determined or if opening the file errors.
pub fn open_psm_file<'a>(
    path: impl AsRef<Path>,
    ontologies: &'a Ontologies,
    keep_all_columns: bool,
) -> Result<GeneralPSMs<'a>, BoxedError<'static, BasicKind>> {
    let path = path.as_ref();
    let actual_extension = path
        .extension()
        .map(|ex| {
            ex.eq_ignore_ascii_case("gz")
                .then_some(path)
                .and_then(|p| p.file_stem())
                .and_then(|p| Path::new(p).extension())
                .unwrap_or(ex)
        })
        .map(|ex| ex.to_string_lossy().to_lowercase());
    match actual_extension.as_deref() {
        Some("csv") => PeaksPSM::parse_file(path, ontologies, keep_all_columns, None)
            .map(PSMIter::into_box)
            .or_else(|pe| {
                NovorPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|ne| (pe, ne))
            })
            .or_else(|(pe, ne)| {
                InstaNovoPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|ie| (pe, ne, ie))
            })
            .or_else(|(pe, ne, ie)| {
                PLinkPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|le| (pe, ne, ie, le))
            })
            .or_else(|(pe, ne, ie, le)| {
                PowerNovoPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|pne| (pe, ne, ie, le, pne))
            })
            .or_else(|(pe, ne, ie, le, pne)| {
                PLGSPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|ple| (pe, ne, ie, le, pne, ple))
            })
            .or_else(|(pe, ne, ie, le, pne, ple)| {
                BasicCSVPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|be| (pe, ne, ie, le, pne, ple, be))
            })
            .or_else(|(pe, ne, ie, le, pne, ple, be)| {
                PUniFindPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|pue| (pe, ne, ie, le, pne, ple, be, pue))
            })
            .map_err(|(pe, ne, ie, le, pne, ple, be, pue)| {
                BoxedError::new(
                    BasicKind::Error,
                    "Unknown file format",
                    "Could not be recognised as either a Peaks, Novor, InstaNovo, pLink, PowerNovo, PLGS, pUniFind, or basic file",
                    Context::default().source(path.to_string_lossy()).to_owned(),
                )
                .add_underlying_errors(vec![pe, ne, ie, le, pne, ple, be, pue])
            }),
        Some("tsv") => SagePSM::parse_file(path, ontologies, keep_all_columns, None)
            .map(PSMIter::into_box)
            .or_else(|se| {
                PepNetPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|pe| (se, pe))
            })
            .or_else(|(se, pe)| {
                MSFraggerPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|mfe| (se, pe, mfe))
            })
            .or_else(|(se, pe, mfe)| {
                ProteoscapePSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|pse| (se, pe, mfe, pse))
            })
            .or_else(|(se, pe, mfe, pse)| {
                NovoBPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|ne| (se, pe, mfe, pse, ne))
            })
            .or_else(|(se, pe, mfe, pse, ne)| {
                PiPrimeNovoPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|pne| (se, pe, mfe, pse, ne, pne))
            })
            .map_err(|(se, pe, mfe, pse, ne, pne)| {
                BoxedError::new(
                    BasicKind::Error,
                    "Unknown file format",
                    "Could not be recognised a Sage, PepNet, MSFragger, NovoB, π-PrimeNovo or Proteoscape file",
                    Context::default().source(path.to_string_lossy()).to_owned(),
                )
                .add_underlying_errors(vec![se, pe, mfe, pse, ne, pne])
            }),
        Some("psmtsv") => MetaMorpheusPSM::parse_file(path, ontologies, keep_all_columns, None)
            .map(PSMIter::into_box)
            .or_else(|me| {
                OpairPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|oe| (me, oe))
            })
            .map_err(|(me, oe)| {
                BoxedError::new(
                    BasicKind::Error,
                    "Unknown file format",
                    "Could not be recognised a MetaMorpheus or OPair file",
                    Context::default().source(path.to_string_lossy()).to_owned(),
                )
                .add_underlying_errors(vec![me, oe])
            }),
        Some("fasta" | "fas" | "fa" | "faa" | "mpfa") => FastaData::parse_file(path).map(|peptides| {
            let a: Box<dyn Iterator<Item = Result<PSM<Linked, MaybePeptidoform>, BoxedError<'static, BasicKind>>> + 'a> =
                Box::new(peptides.into_iter().map(|p| Ok(PSM::<SemiAmbiguous, PeptidoformPresent>::from(p).cast())));
            a
        }),
        #[cfg(feature = "mzannotate")]
        Some("txt")
            if path
                .file_stem()
                .is_some_and(|p| p.to_string_lossy().to_ascii_lowercase().contains(".mzspeclib")) =>
        {
            annotated_spectrum::parse_mzspeclib(path, ontologies)
        }
        Some("txt") => MaxQuantPSM::parse_file(path, ontologies, keep_all_columns, None)
            .map(PSMIter::into_box)
            .or_else(|me| {
                PiHelixNovoPSM::parse_file(path, ontologies, keep_all_columns, None)
                    .map(PSMIter::into_box)
                    .map_err(|hne| (me, hne))
            })
            .map_err(|(me, he)| {
                BoxedError::new(
                    BasicKind::Error,
                    "Unknown file format",
                    "Could not be recognised as either a MaxQuant or π-HelixNovo file",
                    Context::default().source(path.to_string_lossy()).to_owned(),
                )
                .add_underlying_errors(vec![me, he])
            }),
        Some("mztab") => MzTabPSM::parse_file(path, ontologies).map(|(_, _, peptides)| {
            let a: Box<dyn Iterator<Item = Result<PSM<Linked, MaybePeptidoform>, BoxedError<'static, BasicKind>>> + 'a> =
                Box::new(peptides.into_iter().map(|p| p.map(|p| PSM::<SimpleLinear, MaybePeptidoform>::from(p).cast())));
            a
        }),
        Some("deepnovo_denovo") => DeepNovoFamilyPSM::parse_file(path, ontologies, keep_all_columns, None).map(PSMIter::into_box),
        Some("ssl") => SpectrumSequenceListPSM::parse_file(path, ontologies, keep_all_columns, None).map(PSMIter::into_box),
        _ => Err(BoxedError::new(
            BasicKind::Error,
            "Unknown extension",
            "Use CSV, SSL, TSV, TXT, PSMTSV, deepnovo_denovo, or Fasta, or any of these as a gzipped file (eg csv.gz).",
            Context::default().source(path.to_string_lossy()).to_owned(),
        )),
    }
}

/// Define a parameter for an mzTab file
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct CVTerm {
    /// The term
    pub term: Term,
    /// The value
    pub value: Box<str>,
}

impl From<Term> for CVTerm {
    fn from(term: Term) -> Self {
        Self {
            term,
            value: Box::default(),
        }
    }
}

impl std::fmt::Display for CVTerm {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "[{},{},{},{}]",
            self.term.accession.cv, self.term.accession, self.term.name, self.value
        )
    }
}

impl mzcore::space::Space for CVTerm {
    fn space(&self) -> mzcore::space::UsedSpace {
        (self.term.space() + self.value.space()).set_total::<Self>()
    }
}

#[expect(clippy::missing_panics_doc)]
#[cfg(test)]
mod tests {
    use std::{fs::File, io::BufReader};

    use super::*;

    #[test]
    fn open_sage() {
        match test_format::<SagePSM>(
            BufReader::new(File::open("src/test_files/sage_v0_14.tsv").unwrap()),
            &mzcore::ontology::STATIC_ONTOLOGIES,
            TestSettings {
                allow_mass_mods: true,
                ..Default::default()
            },
            Some(SageVersion::V0_14),
        ) {
            Ok(n) => assert_eq!(n, 19),
            Err(e) => {
                println!("{e}");
                panic!("Failed PSM test");
            }
        }
    }

    #[test]
    fn open_msfragger() {
        match test_format::<MSFraggerPSM>(
            BufReader::new(File::open("src/test_files/msfragger_v21.tsv").unwrap()),
            &mzcore::ontology::STATIC_ONTOLOGIES,
            TestSettings {
                allow_mass_mods: true,
                ..Default::default()
            },
            Some(MSFraggerVersion::FragPipeV20Or21),
        ) {
            Ok(n) => assert_eq!(n, 19),
            Err(e) => {
                println!("{e}");
                panic!("Failed PSM test");
            }
        }
    }
}