rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
//! Rust enums for every `xs:restriction` enumeration in the RDML schema.
//!
//! Each enum serialises (in XML *and* via serde) as the exact schema
//! string, exposed through [`as_str`](SampleType::as_str) / `FromStr` /
//! `Display`. There are no catch-all variants: a value outside the schema
//! enumeration is a parse error, reported with its document location.

use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

use crate::error::Error;

macro_rules! schema_enum {
    (
        $(#[$doc:meta])*
        $name:ident: $what:literal {
            $($(#[$vdoc:meta])* $variant:ident => $s:literal),+ $(,)?
        }
    ) => {
        $(#[$doc])*
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
        pub enum $name {
            $(
                $(#[$vdoc])*
                #[serde(rename = $s)]
                $variant,
            )+
        }

        impl $name {
            /// The exact string this value takes in an RDML document.
            pub fn as_str(self) -> &'static str {
                match self {
                    $(Self::$variant => $s,)+
                }
            }

            /// All values of this enumeration.
            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                f.write_str(self.as_str())
            }
        }

        impl FromStr for $name {
            type Err = Error;
            fn from_str(s: &str) -> Result<Self, Error> {
                match s {
                    $($s => Ok(Self::$variant),)+
                    other => Err(Error::InvalidValue(format!(
                        concat!("`{}` is not a valid ", $what, " (expected one of: {})"),
                        other,
                        [$($s),+].join(", "),
                    ))),
                }
            }
        }
    };
}

schema_enum! {
    /// The type of a sample with respect to one target (schema
    /// `sampleTypeType`). If a sample carries no `type` element at all, it
    /// is an unknown sample (`unkn`) — the schema default.
    SampleType: "sample type" {
        /// `unkn` — unknown sample (the default).
        Unknown => "unkn",
        /// `ntc` — no template control.
        NoTemplateControl => "ntc",
        /// `nac` — no amplification control.
        NoAmplificationControl => "nac",
        /// `std` — standard sample (part of a standard curve).
        Standard => "std",
        /// `ntp` — no target present.
        NoTargetPresent => "ntp",
        /// `nrt` — minus reverse transcriptase (−RT) control.
        MinusRt => "nrt",
        /// `pos` — positive control.
        PositiveControl => "pos",
        /// `opt` — optical calibrator sample.
        OpticalCalibrator => "opt",
    }
}

schema_enum! {
    /// Whether a target is measured for its own sake or as a normalisation
    /// reference (schema `targetTypeType`).
    TargetType: "target type" {
        /// `toi` — target of interest.
        TargetOfInterest => "toi",
        /// `ref` — reference target (used for normalisation).
        Reference => "ref",
    }
}

schema_enum! {
    /// The monitoring chemistry of a dye (schema `dyeChemistryType`).
    DyeChemistry: "dye chemistry" {
        /// Non-saturating DNA binding dye (e.g. SYBR Green I).
        NonSaturatingDnaBindingDye => "non-saturating DNA binding dye",
        /// Saturating DNA binding dye (e.g. Eva Green, LC Green Plus, BEBO, `Syto9`).
        SaturatingDnaBindingDye => "saturating DNA binding dye",
        /// Hybridization probe (e.g. Molecular Beacon, Light-Up probe, `BHQnova` probe).
        HybridizationProbe => "hybridization probe",
        /// Hydrolysis probe (e.g. `TaqMan`, `NuPCR`).
        HydrolysisProbe => "hydrolysis probe",
        /// Labelled forward primer (e.g. LUX primer).
        LabelledForwardPrimer => "labelled forward primer",
        /// Labelled reverse primer (e.g. Scorpion probe, Sunrise probe, Amplifluor).
        LabelledReversePrimer => "labelled reverse primer",
        /// DNA-zyme probe (e.g. `QZyme` probe).
        DnaZymeProbe => "DNA-zyme probe",
    }
}

schema_enum! {
    /// What a protocol step's measurement acquires (schema `measureType`).
    Measure: "measure" {
        /// `real time` — store the reading as amplification (real-time) data.
        RealTime => "real time",
        /// `meltcurve` — store the reading as melting-curve data.
        MeltCurve => "meltcurve",
    }
}

schema_enum! {
    /// The kind of nucleotide used as template (schema `nucleotideType`).
    Nucleotide: "nucleotide type" {
        /// `DNA`.
        Dna => "DNA",
        /// `genomic DNA`.
        GenomicDna => "genomic DNA",
        /// `cDNA`.
        Cdna => "cDNA",
        /// `RNA`.
        Rna => "RNA",
    }
}

schema_enum! {
    /// How row or column labels of a PCR format are displayed (schema
    /// `labelFormatType`).
    LabelFormat: "label format" {
        /// `ABC` — letters (A, B, C, …).
        Abc => "ABC",
        /// `123` — numbers (1, 2, 3, …).
        Numbers => "123",
        /// `A1a1` — compound labels for multi-array plates
        /// (outer position + inner position, e.g. `BioTrove`).
        A1a1 => "A1a1",
    }
}

schema_enum! {
    /// The primers used to reverse-transcribe RNA to cDNA (schema
    /// `primingMethodType`).
    PrimingMethod: "priming method" {
        /// `oligo-dt`.
        OligoDt => "oligo-dt",
        /// `random`.
        Random => "random",
        /// `target-specific`.
        TargetSpecific => "target-specific",
        /// `oligo-dt and random`.
        OligoDtAndRandom => "oligo-dt and random",
        /// `other` (added in RDML 1.1).
        Other => "other",
    }
}

schema_enum! {
    /// The unit of a [`Quantity`](crate::Quantity) (schema
    /// `quantityUnitType`). All units must be linear — no exponents or
    /// logarithms.
    QuantityUnit: "quantity unit" {
        /// `cop` — copies per microliter.
        Copies => "cop",
        /// `fold` — fold change.
        Fold => "fold",
        /// `dil` — dilution (10 means a 1:10 dilution).
        Dilution => "dil",
        /// `ng` — nanogram per microliter.
        Nanogram => "ng",
        /// `nMol` — nanomol per microliter.
        NanoMol => "nMol",
        /// `other` — another unit (must still be linear).
        Other => "other",
    }
}

schema_enum! {
    /// The method used to determine the Cq value (schema
    /// `cqDetectionMethodType`).
    CqDetectionMethod: "Cq detection method" {
        /// `automated threshold and baseline settings`.
        AutomatedThresholdAndBaseline => "automated threshold and baseline settings",
        /// `manual threshold and baseline settings`.
        ManualThresholdAndBaseline => "manual threshold and baseline settings",
        /// `second derivative maximum`.
        SecondDerivativeMaximum => "second derivative maximum",
        /// `other`.
        Other => "other",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trip_all_values() {
        fn check<E: Copy + FromStr<Err = Error> + PartialEq + fmt::Debug>(
            all: &[E],
            as_str: impl Fn(E) -> &'static str,
        ) {
            for &v in all {
                assert_eq!(as_str(v).parse::<E>().unwrap(), v);
            }
        }
        check(SampleType::ALL, SampleType::as_str);
        check(TargetType::ALL, TargetType::as_str);
        check(DyeChemistry::ALL, DyeChemistry::as_str);
        check(Measure::ALL, Measure::as_str);
        check(Nucleotide::ALL, Nucleotide::as_str);
        check(LabelFormat::ALL, LabelFormat::as_str);
        check(PrimingMethod::ALL, PrimingMethod::as_str);
        check(QuantityUnit::ALL, QuantityUnit::as_str);
        check(CqDetectionMethod::ALL, CqDetectionMethod::as_str);
    }

    #[test]
    fn rejects_unknown_and_wrong_case() {
        assert!("Unkn".parse::<SampleType>().is_err());
        assert!("TOI".parse::<TargetType>().is_err());
        assert!("melt curve".parse::<Measure>().is_err());
        assert!("dna".parse::<Nucleotide>().is_err());
    }

    #[test]
    fn serde_uses_schema_strings() {
        assert_eq!(
            serde_json::to_string(&DyeChemistry::HydrolysisProbe).unwrap(),
            "\"hydrolysis probe\""
        );
        let v: SampleType = serde_json::from_str("\"ntc\"").unwrap();
        assert_eq!(v, SampleType::NoTemplateControl);
    }

    #[test]
    fn exact_schema_counts() {
        // Pin the enumeration sizes to the 1.3/1.4 XSDs so an accidental
        // variant addition or removal fails loudly.
        assert_eq!(SampleType::ALL.len(), 8);
        assert_eq!(TargetType::ALL.len(), 2);
        assert_eq!(DyeChemistry::ALL.len(), 7);
        assert_eq!(Measure::ALL.len(), 2);
        assert_eq!(Nucleotide::ALL.len(), 4);
        assert_eq!(LabelFormat::ALL.len(), 3);
        assert_eq!(PrimingMethod::ALL.len(), 5);
        assert_eq!(QuantityUnit::ALL.len(), 6);
        assert_eq!(CqDetectionMethod::ALL.len(), 4);
    }
}