rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
//! The in-memory RDML document model.
//!
//! The model mirrors the document: flat and owned. The root [`Rdml`]
//! holds the master elements (`experimenter`, `documentation`, `dye`,
//! `sample`, `target`, `thermalCyclingConditions`, `experiment`), each
//! identified by a unique [`Id`]; other elements point at them through
//! typed references ([`SampleRef`](crate::SampleRef),
//! [`TargetRef`](crate::TargetRef), …) rather than nesting. There is no
//! `Rc`/`RefCell` object graph; resolve references through the lookup
//! methods ([`Rdml::sample`], [`Rdml::target`], …) when needed.
//!
//! The model is shaped like RDML 1.4, a superset of all versions;
//! version differences are handled at read/write time, not in the model.

pub mod dpcr;
pub mod dye;
pub mod experiment;
pub mod people;
pub mod protocol;
pub mod sample;
pub mod target;

use serde::{Deserialize, Serialize};

use crate::types::DateTime;

pub use dpcr::{PartitionData, Partitions};
pub use dye::Dye;
pub use experiment::{
    AmpPoint, Data, DataCollectionSoftware, Experiment, MeltPoint, PcrFormat, PlateLayout, React,
    Run,
};
pub use people::{Documentation, Experimenter, RdmlId};
pub use protocol::{
    GradientStep, LoopStep, PauseStep, Step, StepKind, TemperatureStep, ThermalCyclingConditions,
};
pub use sample::{
    Annotation, CdnaSynthesisMethod, Quantity, Sample, SampleTypeEntry, TemplateQuantity,
};
pub use target::{CommercialAssay, Oligo, Sequences, Target, XRef};

/// A complete RDML document.
///
/// Every field is optional or a list — a default [`Rdml`] is a valid,
/// nearly-empty document. A document carrying measurements pulls in the
/// full required chain: an [`Experiment`] containing [`Run`]s containing
/// [`React`]ions, each referencing a [`Sample`], with [`Data`] per
/// [`Target`], and each target referencing a [`Dye`].
///
/// ```
/// use rdml_qpcr::{Id, Rdml, Sample};
///
/// let mut doc = Rdml::default();
/// doc.samples.push(Sample::new(Id::new("liver biopsy 1")?));
/// assert!(doc.sample("liver biopsy 1").is_some());
/// assert!(doc.validate().is_ok());
/// # Ok::<(), rdml_qpcr::Error>(())
/// ```
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Rdml {
    /// When this file was created.
    #[serde(rename = "dateMade", skip_serializing_if = "Option::is_none", default)]
    pub date_made: Option<DateTime>,
    /// When this file was last updated.
    #[serde(
        rename = "dateUpdated",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub date_updated: Option<DateTime>,
    /// Publisher-assigned identities of the file itself.
    #[serde(rename = "id", skip_serializing_if = "Vec::is_empty", default)]
    pub ids: Vec<RdmlId>,
    /// The experimenters referenced by runs and protocols.
    #[serde(
        rename = "experimenter",
        skip_serializing_if = "Vec::is_empty",
        default
    )]
    pub experimenters: Vec<Experimenter>,
    /// Shared description blocks referenced throughout the document.
    #[serde(
        rename = "documentation",
        skip_serializing_if = "Vec::is_empty",
        default
    )]
    pub documentations: Vec<Documentation>,
    /// The dyes referenced by targets.
    #[serde(rename = "dye", skip_serializing_if = "Vec::is_empty", default)]
    pub dyes: Vec<Dye>,
    /// The samples referenced by reactions.
    #[serde(rename = "sample", skip_serializing_if = "Vec::is_empty", default)]
    pub samples: Vec<Sample>,
    /// The targets referenced by measurement data.
    #[serde(rename = "target", skip_serializing_if = "Vec::is_empty", default)]
    pub targets: Vec<Target>,
    /// The thermal cycling protocols referenced by runs and cDNA
    /// synthesis methods.
    #[serde(
        rename = "thermalCyclingConditions",
        skip_serializing_if = "Vec::is_empty",
        default
    )]
    pub thermal_cycling_conditions: Vec<ThermalCyclingConditions>,
    /// The experiments holding the actual measurements.
    #[serde(rename = "experiment", skip_serializing_if = "Vec::is_empty", default)]
    pub experiments: Vec<Experiment>,
}

impl Rdml {
    /// Creates an empty document.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Looks up a sample by id.
    pub fn sample(&self, id: impl AsRef<str>) -> Option<&Sample> {
        let id = id.as_ref();
        self.samples.iter().find(|s| s.id.as_str() == id)
    }

    /// Looks up a target by id.
    pub fn target(&self, id: impl AsRef<str>) -> Option<&Target> {
        let id = id.as_ref();
        self.targets.iter().find(|t| t.id.as_str() == id)
    }

    /// Looks up a dye by id.
    pub fn dye(&self, id: impl AsRef<str>) -> Option<&Dye> {
        let id = id.as_ref();
        self.dyes.iter().find(|d| d.id.as_str() == id)
    }

    /// Looks up an experimenter by id.
    pub fn experimenter(&self, id: impl AsRef<str>) -> Option<&Experimenter> {
        let id = id.as_ref();
        self.experimenters.iter().find(|e| e.id.as_str() == id)
    }

    /// Looks up a shared documentation block by id.
    pub fn documentation(&self, id: impl AsRef<str>) -> Option<&Documentation> {
        let id = id.as_ref();
        self.documentations.iter().find(|d| d.id.as_str() == id)
    }

    /// Looks up a thermal cycling protocol by id.
    pub fn thermal_cycling(&self, id: impl AsRef<str>) -> Option<&ThermalCyclingConditions> {
        let id = id.as_ref();
        self.thermal_cycling_conditions
            .iter()
            .find(|t| t.id.as_str() == id)
    }

    /// Looks up an experiment by id.
    pub fn experiment(&self, id: impl AsRef<str>) -> Option<&Experiment> {
        let id = id.as_ref();
        self.experiments.iter().find(|e| e.id.as_str() == id)
    }

    /// Iterates over every reaction in the document, with its experiment
    /// and run context.
    pub fn reactions(&self) -> impl Iterator<Item = ReactionContext<'_>> {
        self.experiments.iter().flat_map(|experiment| {
            experiment.runs.iter().flat_map(move |run| {
                run.reacts.iter().map(move |react| ReactionContext {
                    experiment,
                    run,
                    react,
                })
            })
        })
    }

    /// Iterates over every per-target data entry in the document, with
    /// its full experiment / run / reaction context.
    pub fn data_entries(&self) -> impl Iterator<Item = DataContext<'_>> {
        self.reactions().flat_map(|ctx| {
            ctx.react.data.iter().map(move |data| DataContext {
                experiment: ctx.experiment,
                run: ctx.run,
                react: ctx.react,
                data,
            })
        })
    }
}

/// One reaction together with the experiment and run it belongs to;
/// yielded by [`Rdml::reactions`].
#[derive(Debug, Clone, Copy)]
pub struct ReactionContext<'a> {
    /// The experiment containing the run.
    pub experiment: &'a Experiment,
    /// The run containing the reaction.
    pub run: &'a Run,
    /// The reaction.
    pub react: &'a React,
}

/// One per-target data entry together with its full context; yielded by
/// [`Rdml::data_entries`].
#[derive(Debug, Clone, Copy)]
pub struct DataContext<'a> {
    /// The experiment containing the run.
    pub experiment: &'a Experiment,
    /// The run containing the reaction.
    pub run: &'a Run,
    /// The reaction containing the data entry.
    pub react: &'a React,
    /// The data entry (one target's measurements in this reaction).
    pub data: &'a Data,
}