rdml-qpcr 0.1.1

Read, write, and validate RDML (Real-time PCR Data Markup Language) qPCR data files
Documentation
//! XML (de)serialisation of RDML documents.
//!
//! The usual entry points are [`RdmlFile`](crate::RdmlFile) for whole
//! `.rdml`/`.rdm`/`.xml` files, and [`Rdml::from_xml`] / [`Rdml::to_xml`]
//! for working with XML text directly.

mod reader;
mod writer;

pub(crate) use writer::format_float;

use crate::error::{Error, Result};
use crate::model::Rdml;
use crate::version::{RdmlVersion, ReadNote, WriteReport};

/// The result of parsing RDML XML: the document plus everything worth
/// knowing about how it was read.
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedDocument {
    /// The parsed document, migrated to the in-memory (1.4-shaped) model.
    pub document: Rdml,
    /// The version declared by the source file.
    pub version: RdmlVersion,
    /// Everything that was mapped, dropped, or skipped during reading —
    /// empty for a fully clean read. Legacy versions produce migration
    /// notes (see [`crate::version`]); any version produces notes for
    /// unknown (skipped) elements.
    pub notes: Vec<ReadNote>,
}

impl Rdml {
    /// Parses an RDML document (any version, 1.0–1.4) from XML text.
    ///
    /// The reader matches element names regardless of namespace prefix,
    /// accepts children in any order, and skips unknown elements with a
    /// note; malformed values and missing required elements are errors.
    ///
    /// # Errors
    ///
    /// Fails on malformed XML, a non-`rdml` root, a missing or
    /// unsupported `version` attribute, and invalid values (bad numbers,
    /// dates, enumeration values, missing required elements) — each with
    /// a document path.
    ///
    /// ```
    /// use rdml_qpcr::Rdml;
    ///
    /// let parsed = Rdml::from_xml(r#"<?xml version="1.0" encoding="UTF-8"?>
    /// <rdml xmlns="http://www.rdml.org" version="1.3">
    ///   <sample id="liver 1"/>
    /// </rdml>"#)?;
    /// assert_eq!(parsed.version, rdml_qpcr::RdmlVersion::V1_3);
    /// assert!(parsed.document.sample("liver 1").is_some());
    /// # Ok::<(), rdml_qpcr::Error>(())
    /// ```
    pub fn from_xml(xml: &str) -> Result<ParsedDocument> {
        reader::parse(xml)
    }

    /// Serialises the document as RDML XML of the given version,
    /// validating first.
    ///
    /// Returns the XML text and a [`WriteReport`] listing anything the
    /// target version could not express (empty when nothing was dropped).
    ///
    /// # Errors
    ///
    /// Fails with [`Error::Validation`] if [`validate`](Rdml::validate)
    /// finds errors — use [`to_xml_unchecked`](Rdml::to_xml_unchecked) to
    /// skip that gate deliberately — and with
    /// [`Error::UnsupportedWriteVersion`] for RDML 1.0.
    pub fn to_xml(&self, version: RdmlVersion) -> Result<(String, WriteReport)> {
        if let Err(report) = self.validate() {
            return Err(Error::Validation(report));
        }
        self.to_xml_unchecked(version)
    }

    /// Serialises without validating first. The output of an invalid
    /// document may violate the RDML schema; prefer
    /// [`to_xml`](Rdml::to_xml).
    ///
    /// # Errors
    ///
    /// Fails with [`Error::UnsupportedWriteVersion`] for RDML 1.0.
    pub fn to_xml_unchecked(&self, version: RdmlVersion) -> Result<(String, WriteReport)> {
        let mut out = Vec::new();
        let report = writer::write_xml(self, &mut out, version)?;
        // The writer emits only UTF-8; the lossy branch is unreachable
        // and exists to avoid a panic path.
        let xml = match String::from_utf8(out) {
            Ok(xml) => xml,
            Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
        };
        Ok((xml, report))
    }
}