rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
//! Targets: the PCR reactions being measured (root-level master element
//! `target`).

use serde::{Deserialize, Serialize};

use crate::enums::TargetType;
use crate::types::{DocumentationRef, DyeRef, Id, Sequence, TargetRef};

/// A defined PCR reaction.
///
/// PCR reactions for the same gene that differ in primer sequences are
/// considered *different* targets.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Target {
    /// Unique id this target is referenced by, e.g. the assay or gene
    /// name. Short human-readable name; see [`Id`].
    pub id: Id,
    /// Free-text description.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub description: Option<String>,
    /// References to shared [`Documentation`](crate::Documentation) blocks.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub documentation: Vec<DocumentationRef>,
    /// References to entries in external databases.
    #[serde(rename = "xRef", skip_serializing_if = "Vec::is_empty", default)]
    pub x_refs: Vec<XRef>,
    /// Whether this is a target of interest or a normalisation reference.
    #[serde(rename = "type")]
    pub target_type: TargetType,
    /// Free-text description of how the expected amplification efficiency
    /// was determined. *(RDML ≥ 1.1)*
    #[serde(
        rename = "amplificationEfficiencyMethod",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub amplification_efficiency_method: Option<String>,
    /// The *expected* amplification efficiency, as the fold-increase of
    /// DNA per cycle (the base of the exponential function) — e.g. `1.95`
    /// for 95 % efficiency. Per-reaction observed efficiency lives in
    /// [`Data::amp_eff`](crate::Data::amp_eff).
    #[serde(
        rename = "amplificationEfficiency",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub amplification_efficiency: Option<f64>,
    /// Standard error of [`amplification_efficiency`](Self::amplification_efficiency).
    /// *(RDML ≥ 1.2)*
    #[serde(
        rename = "amplificationEfficiencySE",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub amplification_efficiency_se: Option<f64>,
    /// The expected melting temperature of the amplicon in °C.
    /// *(RDML ≥ 1.3)*
    #[serde(
        rename = "meltingTemperature",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub melting_temperature: Option<f64>,
    /// Detection limit in copies per microliter.
    #[serde(
        rename = "detectionLimit",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub detection_limit: Option<f64>,
    /// The dye reporting this target. Required since RDML 1.1.
    #[serde(rename = "dyeId")]
    pub dye_id: DyeRef,
    /// Primer, probe, and amplicon sequences.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub sequences: Option<Sequences>,
    /// For commercial assays whose primer sequences are proprietary.
    #[serde(
        rename = "commercialAssay",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub commercial_assay: Option<CommercialAssay>,
}

impl Target {
    /// Creates a target with the required fields and no optional detail.
    #[must_use]
    pub fn new(id: Id, target_type: TargetType, dye_id: DyeRef) -> Self {
        Self {
            id,
            description: None,
            documentation: Vec::new(),
            x_refs: Vec::new(),
            target_type,
            amplification_efficiency_method: None,
            amplification_efficiency: None,
            amplification_efficiency_se: None,
            melting_temperature: None,
            detection_limit: None,
            dye_id,
            sequences: None,
            commercial_assay: None,
        }
    }

    /// A typed reference to this target, for use in
    /// [`Data::tar`](crate::Data::tar) and target-scoped sample entries.
    #[must_use]
    pub fn reference(&self) -> TargetRef {
        self.id.clone().into()
    }
}

/// A reference to an entry in an external database (schema `xRefType`),
/// e.g. name `GenBank`, id `AJ832138`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct XRef {
    /// The external database, e.g. `GenBank`.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub name: Option<String>,
    /// The id of the entry within that database.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub id: Option<String>,
}

impl XRef {
    /// Creates a cross-reference with both parts set.
    pub fn new(name: impl Into<String>, id: impl Into<String>) -> Self {
        Self {
            name: Some(name.into()),
            id: Some(id.into()),
        }
    }
}

/// The oligonucleotides of a target (schema `sequencesType`).
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Sequences {
    /// The forward primer.
    #[serde(
        rename = "forwardPrimer",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub forward_primer: Option<Oligo>,
    /// The reverse primer.
    #[serde(
        rename = "reversePrimer",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub reverse_primer: Option<Oligo>,
    /// The (first) probe.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub probe1: Option<Oligo>,
    /// The second probe (e.g. for hybridization probe pairs).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub probe2: Option<Oligo>,
    /// The amplicon sequence.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub amplicon: Option<Oligo>,
}

/// One oligonucleotide (schema `oligoType`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Oligo {
    /// Description of a 3′ modification, if present.
    #[serde(
        rename = "threePrimeTag",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub three_prime_tag: Option<String>,
    /// Description of a 5′ modification, if present.
    #[serde(
        rename = "fivePrimeTag",
        skip_serializing_if = "Option::is_none",
        default
    )]
    pub five_prime_tag: Option<String>,
    /// The sequence itself (IUPAC alphabet).
    pub sequence: Sequence,
    /// Nanomolar (nmol/l) concentration of this oligo in the reaction;
    /// typical range 10–1000 nmol/l. If absent, consumers assume 250.
    /// *(RDML 1.4, candidate recommendation)*
    #[serde(rename = "oligoConc", skip_serializing_if = "Option::is_none", default)]
    pub oligo_conc: Option<f64>,
}

impl Oligo {
    /// Creates an oligo from its sequence, with no tags.
    #[must_use]
    pub fn new(sequence: Sequence) -> Self {
        Self {
            three_prime_tag: None,
            five_prime_tag: None,
            sequence,
            oligo_conc: None,
        }
    }
}

/// A commercial assay whose primer sequences may be unknown (schema
/// `commercialAssayType`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CommercialAssay {
    /// The company selling the assay.
    pub company: String,
    /// The company's order number for the assay.
    #[serde(rename = "orderNumber")]
    pub order_number: String,
}

impl CommercialAssay {
    /// Creates a commercial assay description.
    pub fn new(company: impl Into<String>, order_number: impl Into<String>) -> Self {
        Self {
            company: company.into(),
            order_number: order_number.into(),
        }
    }
}