use std::num::NonZeroU32;
use quick_xml::Reader;
use quick_xml::events::{BytesStart, Event};
use crate::enums::{Nucleotide, QuantityUnit};
use crate::error::{Error, Result};
use crate::model::{
AmpPoint, Annotation, CdnaSynthesisMethod, CommercialAssay, Data, DataCollectionSoftware,
Documentation, Dye, Experiment, Experimenter, GradientStep, LoopStep, MeltPoint, Oligo,
PartitionData, Partitions, PauseStep, PcrFormat, Quantity, Rdml, RdmlId, React, Run, Sample,
SampleTypeEntry, Sequences, Step, StepKind, Target, TemperatureStep, TemplateQuantity,
ThermalCyclingConditions, XRef,
};
use crate::types::{DateTime, DyeRef, Id, Reasons, Sequence, TargetRef};
use crate::version::{RdmlVersion, ReadNote};
use super::ParsedDocument;
pub(crate) fn parse(xml: &str) -> Result<ParsedDocument> {
let reader = Reader::from_str(xml.trim_start_matches('\u{feff}'));
let mut p = Parser {
r: reader,
version: RdmlVersion::LATEST,
notes: Vec::new(),
};
p.document()
}
struct Parser<'x> {
r: Reader<&'x [u8]>,
version: RdmlVersion,
notes: Vec<ReadNote>,
}
fn name(e: &BytesStart<'_>) -> String {
String::from_utf8_lossy(e.local_name().as_ref()).into_owned()
}
impl Parser<'_> {
#[allow(clippy::unused_self)]
fn invalid(&self, path: &str, message: impl Into<String>) -> Error {
Error::Invalid {
path: path.to_string(),
message: message.into(),
}
}
fn note(&mut self, path: impl Into<String>, message: impl Into<String>) {
self.notes.push(ReadNote::new(path, message));
}
fn attr(&self, e: &BytesStart<'_>, key: &[u8], path: &str) -> Result<Option<String>> {
for a in e.attributes() {
let a = a.map_err(|err| self.invalid(path, format!("bad attribute: {err}")))?;
if a.key.local_name().as_ref() == key {
let value = a
.decoded_and_normalized_value(
quick_xml::XmlVersion::Implicit1_0,
self.r.decoder(),
)
.map_err(|err| self.invalid(path, format!("bad attribute value: {err}")))?;
return Ok(Some(value.into_owned()));
}
}
Ok(None)
}
fn id_attr(&self, e: &BytesStart<'_>, path: &str) -> Result<Id> {
let raw = self
.attr(e, b"id", path)?
.ok_or_else(|| self.invalid(path, "missing required attribute `id`"))?;
Id::new(raw).map_err(|err| self.invalid(path, err.to_string()))
}
fn text_content(&mut self, path: &str) -> Result<String> {
let mut out = String::new();
loop {
match self.r.read_event()? {
Event::Text(t) => out.push_str(
&t.xml10_content()
.map_err(|err| self.invalid(path, format!("bad text: {err}")))?,
),
Event::CData(c) => out.push_str(
&c.decode()
.map_err(|err| self.invalid(path, format!("bad CDATA: {err}")))?,
),
Event::GeneralRef(r) => {
let resolved = r
.resolve_char_ref()
.map_err(|err| {
self.invalid(path, format!("bad character reference: {err}"))
})?
.map(String::from)
.or_else(|| {
let name = r.decode().ok()?;
quick_xml::escape::resolve_predefined_entity(&name).map(str::to_string)
});
match resolved {
Some(s) => out.push_str(&s),
None => {
return Err(
self.invalid(path, "undefined entity reference in text value")
);
}
}
}
Event::End(_) => return Ok(out),
Event::Start(e) | Event::Empty(e) => {
return Err(self.invalid(
path,
format!("unexpected element <{}> inside a text value", name(&e)),
));
}
Event::Eof => return Err(self.invalid(path, "unexpected end of document")),
_ => {}
}
}
}
fn content(&mut self, empty: bool, path: &str) -> Result<String> {
if empty {
Ok(String::new())
} else {
self.text_content(path)
}
}
fn skip_unknown(&mut self, e: &BytesStart<'_>, empty: bool, path: &str) -> Result<()> {
let el = name(e);
if !empty {
self.r.read_to_end(e.name())?;
}
self.note(path, format!("skipped unknown element <{el}>"));
Ok(())
}
fn parse_f64(&self, text: &str, path: &str) -> Result<f64> {
text.trim().parse().map_err(|_| {
self.invalid(
path,
format!("`{text}` is not a valid floating-point number"),
)
})
}
fn parse_i32(&self, text: &str, path: &str) -> Result<i32> {
text.trim()
.parse()
.map_err(|_| self.invalid(path, format!("`{text}` is not a valid integer")))
}
fn parse_u32_nonzero(&self, text: &str, path: &str) -> Result<NonZeroU32> {
text.trim()
.parse()
.map_err(|_| self.invalid(path, format!("`{text}` is not a valid positive integer")))
}
fn parse_u32(&self, text: &str, path: &str) -> Result<u32> {
text.trim().parse().map_err(|_| {
self.invalid(
path,
format!("`{text}` is not a valid non-negative integer"),
)
})
}
fn parse_bool(&self, text: &str, path: &str) -> Result<bool> {
match text.trim() {
"true" | "1" => Ok(true),
"false" | "0" => Ok(false),
other => Err(self.invalid(path, format!("`{other}` is not a valid boolean"))),
}
}
fn parse_enum<T: std::str::FromStr<Err = Error>>(&self, text: &str, path: &str) -> Result<T> {
text.trim().parse().map_err(|err| match err {
Error::InvalidValue(m) => self.invalid(path, m),
other => other,
})
}
fn parse_enum_lenient<T: std::str::FromStr<Err = Error>>(
&mut self,
text: &str,
path: &str,
) -> Option<T> {
match text.trim().parse() {
Ok(v) => Some(v),
Err(err) => {
let detail = match err {
Error::InvalidValue(m) => m,
other => other.to_string(),
};
self.note(
path,
format!("dropped schema-invalid optional value: {detail}"),
);
None
}
}
}
fn parse_datetime(&self, text: &str, path: &str) -> Result<DateTime> {
DateTime::new(text.trim()).map_err(|err| match err {
Error::InvalidValue(m) => self.invalid(path, m),
other => other,
})
}
#[allow(clippy::float_cmp)]
fn sentinel_f64(&self, text: &str, path: &str) -> Result<Option<f64>> {
let v = self.parse_f64(text, path)?;
Ok((v != -1.0).then_some(v))
}
fn ref_id(&mut self, e: &BytesStart<'_>, empty: bool, path: &str) -> Result<String> {
let id = self
.attr(e, b"id", path)?
.ok_or_else(|| self.invalid(path, "missing required attribute `id`"))?;
if !empty {
self.r.read_to_end(e.name())?;
}
Ok(id)
}
fn make_ref<T>(&self, raw: String, path: &str) -> Result<T>
where
T: From<Id>,
{
Id::new(raw)
.map(T::from)
.map_err(|err| self.invalid(path, err.to_string()))
}
#[allow(clippy::too_many_lines)]
fn document(&mut self) -> Result<ParsedDocument> {
let (root, root_empty) = loop {
match self.r.read_event()? {
Event::Start(e) => break (e.into_owned(), false),
Event::Empty(e) => break (e.into_owned(), true),
Event::Decl(_)
| Event::Comment(_)
| Event::DocType(_)
| Event::PI(_)
| Event::Text(_) => {}
Event::Eof => {
return Err(Error::NotRdml {
reason: "no XML root element found".into(),
});
}
Event::End(_) | Event::CData(_) | Event::GeneralRef(_) => {
return Err(Error::NotRdml {
reason: "unexpected content before any root element".into(),
});
}
}
};
if root.local_name().as_ref() != b"rdml" {
return Err(Error::NotRdml {
reason: format!("root element is `{}`, not `rdml`", name(&root)),
});
}
let version_attr = self
.attr(&root, b"version", "rdml")?
.ok_or(Error::MissingVersion)?;
self.version = version_attr.parse()?;
let mut doc = Rdml::default();
let mut legacy_dye_refs: Vec<String> = Vec::new();
if !root_empty {
loop {
let (e, empty) = match self.r.read_event()? {
Event::Start(e) => (e.into_owned(), false),
Event::Empty(e) => (e.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid("rdml", "unexpected end of document")),
_ => continue,
};
match e.local_name().as_ref() {
b"dateMade" => {
let t = self.content(empty, "rdml/dateMade")?;
doc.date_made = Some(self.parse_datetime(&t, "rdml/dateMade")?);
}
b"dateUpdated" => {
let t = self.content(empty, "rdml/dateUpdated")?;
doc.date_updated = Some(self.parse_datetime(&t, "rdml/dateUpdated")?);
}
b"id" => doc.ids.push(self.rdml_id(&e, empty)?),
b"experimenter" => doc.experimenters.push(self.experimenter(&e, empty)?),
b"documentation" => doc.documentations.push(self.documentation(&e, empty)?),
b"dye" => doc.dyes.push(self.dye(&e, empty)?),
b"sample" => doc.samples.push(self.sample(&e, empty)?),
b"target" => {
let target = self.target(&e, empty, &mut legacy_dye_refs)?;
doc.targets.push(target);
}
b"thermalCyclingConditions" => {
doc.thermal_cycling_conditions.push(self.tcc(&e, empty)?);
}
b"experiment" => doc.experiments.push(self.experiment(&e, empty)?),
b"thirdPartyExtensions" if self.version == RdmlVersion::V1_0 => {
let raw = if empty {
String::new()
} else {
let text = self.r.read_text(e.name())?;
text.decode()
.map_err(|err| {
self.invalid(
"rdml/thirdPartyExtensions",
format!("bad content: {err}"),
)
})?
.into_owned()
};
self.note(
"rdml/thirdPartyExtensions",
format!(
"dropped (removed in RDML 1.1; extensions belong in separate \
archive members). Raw content: {raw}"
),
);
}
_ => self.skip_unknown(&e, empty, "rdml")?,
}
}
}
if self.version == RdmlVersion::V1_0 {
legacy_dye_refs.sort();
legacy_dye_refs.dedup();
for dye_name in legacy_dye_refs {
let id = Id::new(dye_name.clone())
.map_err(|err| self.invalid("rdml/target/dyeId", err.to_string()))?;
doc.dyes.push(Dye::new(id));
self.note(
format!("rdml/dye[{dye_name}]"),
"synthesized dye master element (RDML 1.0 had none; \
targets carried free-text dye names)",
);
}
}
Ok(ParsedDocument {
document: doc,
version: self.version,
notes: std::mem::take(&mut self.notes),
})
}
fn rdml_id(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<RdmlId> {
let path = "rdml/id";
let mut publisher = None;
let mut serial = None;
let mut hash = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"publisher" => publisher = Some(self.content(cempty, path)?),
b"serialNumber" => serial = Some(self.content(cempty, path)?),
b"MD5Hash" => hash = Some(self.content(cempty, path)?),
_ => self.skip_unknown(&c, cempty, path)?,
}
}
}
let _ = e;
Ok(RdmlId {
publisher: publisher
.ok_or_else(|| self.invalid(path, "missing required element <publisher>"))?,
serial_number: serial
.ok_or_else(|| self.invalid(path, "missing required element <serialNumber>"))?,
md5_hash: hash,
})
}
fn experimenter(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<Experimenter> {
let id = self.id_attr(e, "rdml/experimenter")?;
let path = format!("rdml/experimenter[{id}]");
let mut first = None;
let mut last = None;
let mut email = None;
let mut lab_name = None;
let mut lab_address = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"firstName" => first = Some(self.content(cempty, &path)?),
b"lastName" => last = Some(self.content(cempty, &path)?),
b"email" => email = Some(self.content(cempty, &path)?),
b"labName" => lab_name = Some(self.content(cempty, &path)?),
b"labAddress" => lab_address = Some(self.content(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
let mut out = Experimenter::new(
id,
first.ok_or_else(|| self.invalid(&path, "missing required element <firstName>"))?,
last.ok_or_else(|| self.invalid(&path, "missing required element <lastName>"))?,
);
out.email = email;
out.lab_name = lab_name;
out.lab_address = lab_address;
Ok(out)
}
fn documentation(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<Documentation> {
let id = self.id_attr(e, "rdml/documentation")?;
let path = format!("rdml/documentation[{id}]");
let mut out = Documentation::new(id);
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"text" => out.text = Some(self.content(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
fn dye(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<Dye> {
let id = self.id_attr(e, "rdml/dye")?;
let path = format!("rdml/dye[{id}]");
let mut out = Dye::new(id);
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"description" => out.description = Some(self.content(cempty, &path)?),
b"dyeChemistry" => {
let t = self.content(cempty, &path)?;
out.chemistry =
self.parse_enum_lenient(&t, &format!("{path}/dyeChemistry"));
}
b"nCopyFact" => {
let t = self.content(cempty, &path)?;
out.n_copy_fact = Some(self.parse_f64(&t, &format!("{path}/nCopyFact"))?);
}
b"dyeConc" => {
let t = self.content(cempty, &path)?;
out.dye_conc = Some(self.parse_f64(&t, &format!("{path}/dyeConc"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
fn sample(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<Sample> {
let id = self.id_attr(e, "rdml/sample")?;
let path = format!("rdml/sample[{id}]");
let mut out = Sample::new(id);
let mut legacy: Vec<(String, LegacyTemplate)> = Vec::new();
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
let cname = c.local_name().as_ref().to_vec();
match cname.as_slice() {
b"description" => out.description = Some(self.content(cempty, &path)?),
b"documentation" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/documentation"))?;
out.documentation
.push(self.make_ref(raw, &format!("{path}/documentation"))?);
}
b"xRef" => out.x_refs.push(self.xref(cempty, &path)?),
b"annotation" => out.annotations.push(self.annotation(cempty, &path)?),
b"type" => {
let tpath = format!("{path}/type");
let target_id = match self.attr(&c, b"targetId", &tpath)? {
Some(raw) => Some(self.make_ref(raw, &tpath)?),
None => None,
};
let t = self.content(cempty, &tpath)?;
if let Some(value) = self.parse_enum_lenient(&t, &tpath) {
out.types.push(SampleTypeEntry { value, target_id });
}
}
b"interRunCalibrator" => {
let t = self.content(cempty, &path)?;
out.inter_run_calibrator =
Some(self.parse_bool(&t, &format!("{path}/interRunCalibrator"))?);
}
b"doubleStranded" => {
let t = self.content(cempty, &path)?;
out.double_stranded =
Some(self.parse_bool(&t, &format!("{path}/doubleStranded"))?);
}
b"quantity" => {
let qpath = format!("{path}/quantity");
let target_id = match self.attr(&c, b"targetId", &qpath)? {
Some(raw) => Some(self.make_ref(raw, &qpath)?),
None => None,
};
let (value, unit) = self.quantity_body(cempty, &qpath)?;
out.quantities.push(Quantity {
value,
unit,
target_id,
});
}
b"calibratorSample" => {
let t = self.content(cempty, &path)?;
out.calibrator_sample =
Some(self.parse_bool(&t, &format!("{path}/calibratorSample"))?);
}
b"cdnaSynthesisMethod" => {
out.cdna_synthesis_method = Some(self.cdna_synthesis(cempty, &path)?);
}
b"templateQuantity" => {
out.template_quantity = Some(self.template_quantity(cempty, &path)?);
}
b"templateRNAQuantity" | b"templateDNAQuantity"
if self.version <= RdmlVersion::V1_1 =>
{
let el = String::from_utf8_lossy(&cname).into_owned();
let qpath = format!("{path}/{el}");
let value = if self.version == RdmlVersion::V1_0 {
let t = self.content(cempty, &qpath)?;
(self.parse_f64(&t, &qpath)?, QuantityUnit::Nanogram)
} else {
self.quantity_body(cempty, &qpath)?
};
legacy.push((el, LegacyTemplate::Quantity(value.0, value.1)));
}
b"templateRNAQuality" | b"templateDNAQuality"
if self.version <= RdmlVersion::V1_1 =>
{
let el = String::from_utf8_lossy(&cname).into_owned();
let qpath = format!("{path}/{el}");
let quality = self.template_quality(cempty, &qpath)?;
legacy.push((el, quality));
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
self.migrate_legacy_template(&mut out, legacy, &path);
Ok(out)
}
fn quantity_body(&mut self, empty: bool, path: &str) -> Result<(f64, QuantityUnit)> {
let mut value = None;
let mut unit = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"value" => {
let t = self.content(cempty, path)?;
value = Some(self.parse_f64(&t, &format!("{path}/value"))?);
}
b"unit" => {
let t = self.content(cempty, path)?;
unit = Some(self.parse_enum(&t, &format!("{path}/unit"))?);
}
_ => self.skip_unknown(&c, cempty, path)?,
}
}
}
Ok((
value.ok_or_else(|| self.invalid(path, "missing required element <value>"))?,
unit.ok_or_else(|| self.invalid(path, "missing required element <unit>"))?,
))
}
fn template_quality(&mut self, empty: bool, path: &str) -> Result<LegacyTemplate> {
let mut method = None;
let mut result = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"method" => method = Some(self.content(cempty, path)?),
b"result" => {
let t = self.content(cempty, path)?;
result = Some(self.parse_f64(&t, &format!("{path}/result"))?);
}
_ => self.skip_unknown(&c, cempty, path)?,
}
}
}
Ok(LegacyTemplate::Quality {
method: method
.ok_or_else(|| self.invalid(path, "missing required element <method>"))?,
result: result
.ok_or_else(|| self.invalid(path, "missing required element <result>"))?,
})
}
fn migrate_legacy_template(
&mut self,
sample: &mut Sample,
legacy: Vec<(String, LegacyTemplate)>,
path: &str,
) {
for (element, item) in legacy {
let is_rna = element.contains("RNA");
match item {
LegacyTemplate::Quantity(value, unit) => {
let nucleotide = if is_rna {
Nucleotide::Rna
} else {
Nucleotide::Dna
};
if unit == QuantityUnit::Nanogram && sample.template_quantity.is_none() {
sample.template_quantity = Some(TemplateQuantity {
conc: value,
nucleotide,
});
self.note(
format!("{path}/{element}"),
format!(
"migrated to templateQuantity (conc {value} ng/µl, {nucleotide})"
),
);
} else {
let text = format!("{value} {unit}");
sample.annotations.push(Annotation::new(&element, &text));
let why = if unit == QuantityUnit::Nanogram {
"templateQuantity already occupied".to_string()
} else {
format!("unit `{unit}` has no templateQuantity equivalent")
};
self.note(
format!("{path}/{element}"),
format!("kept as annotation `{element}` = `{text}` ({why})"),
);
}
}
LegacyTemplate::Quality { method, result } => {
sample
.annotations
.push(Annotation::new(format!("{element}/method"), &method));
sample.annotations.push(Annotation::new(
format!("{element}/result"),
result.to_string(),
));
self.note(
format!("{path}/{element}"),
format!(
"migrated to annotations `{element}/method` = `{method}`, \
`{element}/result` = `{result}` (the destination the 1.1→1.2 \
changelog names for quality information)"
),
);
}
}
}
}
fn xref(&mut self, empty: bool, parent: &str) -> Result<XRef> {
let path = format!("{parent}/xRef");
let mut out = XRef::default();
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"name" => out.name = Some(self.content(cempty, &path)?),
b"id" => out.id = Some(self.content(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
fn annotation(&mut self, empty: bool, parent: &str) -> Result<Annotation> {
let path = format!("{parent}/annotation");
let mut property = None;
let mut value = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"property" => property = Some(self.content(cempty, &path)?),
b"value" => value = Some(self.content(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(Annotation {
property: property
.ok_or_else(|| self.invalid(&path, "missing required element <property>"))?,
value: value.ok_or_else(|| self.invalid(&path, "missing required element <value>"))?,
})
}
fn cdna_synthesis(&mut self, empty: bool, parent: &str) -> Result<CdnaSynthesisMethod> {
let path = format!("{parent}/cdnaSynthesisMethod");
let mut out = CdnaSynthesisMethod::default();
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"enzyme" => out.enzyme = Some(self.content(cempty, &path)?),
b"primingMethod" => {
let t = self.content(cempty, &path)?;
out.priming_method =
self.parse_enum_lenient(&t, &format!("{path}/primingMethod"));
}
b"dnaseTreatment" => {
let t = self.content(cempty, &path)?;
out.dnase_treatment =
Some(self.parse_bool(&t, &format!("{path}/dnaseTreatment"))?);
}
b"thermalCyclingConditions" => {
let raw =
self.ref_id(&c, cempty, &format!("{path}/thermalCyclingConditions"))?;
out.thermal_cycling_conditions =
Some(self.make_ref(raw, &format!("{path}/thermalCyclingConditions"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
fn template_quantity(&mut self, empty: bool, parent: &str) -> Result<TemplateQuantity> {
let path = format!("{parent}/templateQuantity");
let mut conc = None;
let mut nucleotide = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"conc" => {
let t = self.content(cempty, &path)?;
conc = Some(self.parse_f64(&t, &format!("{path}/conc"))?);
}
b"nucleotide" => {
let t = self.content(cempty, &path)?;
nucleotide = Some(self.parse_enum(&t, &format!("{path}/nucleotide"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(TemplateQuantity {
conc: conc.ok_or_else(|| self.invalid(&path, "missing required element <conc>"))?,
nucleotide: nucleotide
.ok_or_else(|| self.invalid(&path, "missing required element <nucleotide>"))?,
})
}
#[allow(clippy::too_many_lines)]
fn target(
&mut self,
e: &BytesStart<'_>,
empty: bool,
legacy_dye_refs: &mut Vec<String>,
) -> Result<Target> {
let id = self.id_attr(e, "rdml/target")?;
let path = format!("rdml/target[{id}]");
let mut description = None;
let mut documentation = Vec::new();
let mut x_refs = Vec::new();
let mut target_type = None;
let mut ampl_method = None;
let mut ampl_eff = None;
let mut ampl_eff_se = None;
let mut melting_temperature = None;
let mut detection_limit = None;
let mut dye_id: Option<DyeRef> = None;
let mut sequences = None;
let mut commercial_assay = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"description" => description = Some(self.content(cempty, &path)?),
b"documentation" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/documentation"))?;
documentation.push(self.make_ref(raw, &format!("{path}/documentation"))?);
}
b"xRef" => x_refs.push(self.xref(cempty, &path)?),
b"type" => {
let t = self.content(cempty, &path)?;
target_type = Some(self.parse_enum(&t, &format!("{path}/type"))?);
}
b"amplificationEfficiencyMethod" => {
ampl_method = Some(self.content(cempty, &path)?);
}
b"amplificationEfficiency" => {
let t = self.content(cempty, &path)?;
ampl_eff =
Some(self.parse_f64(&t, &format!("{path}/amplificationEfficiency"))?);
}
b"amplificationEfficiencySE" => {
let t = self.content(cempty, &path)?;
ampl_eff_se =
Some(self.parse_f64(&t, &format!("{path}/amplificationEfficiencySE"))?);
}
b"meltingTemperature" => {
let t = self.content(cempty, &path)?;
melting_temperature =
Some(self.parse_f64(&t, &format!("{path}/meltingTemperature"))?);
}
b"detectionLimit" => {
let t = self.content(cempty, &path)?;
detection_limit =
Some(self.parse_f64(&t, &format!("{path}/detectionLimit"))?);
}
b"dyeId" => {
let dpath = format!("{path}/dyeId");
let raw = if self.version == RdmlVersion::V1_0 {
self.content(cempty, &dpath)?
} else {
self.ref_id(&c, cempty, &dpath)?
};
if self.version == RdmlVersion::V1_0 {
legacy_dye_refs.push(raw.clone());
}
dye_id = Some(self.make_ref(raw, &dpath)?);
}
b"sequences" => sequences = Some(self.sequences(cempty, &path)?),
b"commercialAssay" => {
commercial_assay = Some(self.commercial_assay(cempty, &path)?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
let dye_id = match dye_id {
Some(d) => d,
None if self.version == RdmlVersion::V1_0 => {
let placeholder = "conversion_dye_missing";
legacy_dye_refs.push(placeholder.to_string());
self.note(
format!("{path}/dyeId"),
format!(
"target has no dye (legal in RDML 1.0); assigned synthesized \
dye `{placeholder}`"
),
);
DyeRef::new(placeholder).expect("non-empty")
}
None => return Err(self.invalid(&path, "missing required element <dyeId>")),
};
let mut out = Target::new(
id,
target_type.ok_or_else(|| self.invalid(&path, "missing required element <type>"))?,
dye_id,
);
out.description = description;
out.documentation = documentation;
out.x_refs = x_refs;
out.amplification_efficiency_method = ampl_method;
out.amplification_efficiency = ampl_eff;
out.amplification_efficiency_se = ampl_eff_se;
out.melting_temperature = melting_temperature;
out.detection_limit = detection_limit;
out.sequences = sequences;
out.commercial_assay = commercial_assay;
Ok(out)
}
fn sequences(&mut self, empty: bool, parent: &str) -> Result<Sequences> {
let path = format!("{parent}/sequences");
let mut out = Sequences::default();
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
let slot = match c.local_name().as_ref() {
b"forwardPrimer" => Some(Slot::Forward),
b"reversePrimer" => Some(Slot::Reverse),
b"probe1" => Some(Slot::Probe1),
b"probe2" => Some(Slot::Probe2),
b"amplicon" => Some(Slot::Amplicon),
_ => None,
};
match slot {
Some(slot) => {
let oligo = self.oligo(cempty, &format!("{path}/{}", slot.name()))?;
match slot {
Slot::Forward => out.forward_primer = Some(oligo),
Slot::Reverse => out.reverse_primer = Some(oligo),
Slot::Probe1 => out.probe1 = Some(oligo),
Slot::Probe2 => out.probe2 = Some(oligo),
Slot::Amplicon => out.amplicon = Some(oligo),
}
}
None => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
fn oligo(&mut self, empty: bool, path: &str) -> Result<Oligo> {
let mut three = None;
let mut five = None;
let mut sequence = None;
let mut conc = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"threePrimeTag" => three = Some(self.content(cempty, path)?),
b"fivePrimeTag" => five = Some(self.content(cempty, path)?),
b"sequence" => {
let t = self.content(cempty, path)?;
sequence = Some(Sequence::new(t).map_err(|err| match err {
Error::InvalidValue(m) => self.invalid(&format!("{path}/sequence"), m),
other => other,
})?);
}
b"oligoConc" => {
let t = self.content(cempty, path)?;
conc = Some(self.parse_f64(&t, &format!("{path}/oligoConc"))?);
}
_ => self.skip_unknown(&c, cempty, path)?,
}
}
}
let mut out = Oligo::new(
sequence.ok_or_else(|| self.invalid(path, "missing required element <sequence>"))?,
);
out.three_prime_tag = three;
out.five_prime_tag = five;
out.oligo_conc = conc;
Ok(out)
}
fn commercial_assay(&mut self, empty: bool, parent: &str) -> Result<CommercialAssay> {
let path = format!("{parent}/commercialAssay");
let mut company = None;
let mut order = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"company" => company = Some(self.content(cempty, &path)?),
b"orderNumber" => order = Some(self.content(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(CommercialAssay {
company: company
.ok_or_else(|| self.invalid(&path, "missing required element <company>"))?,
order_number: order
.ok_or_else(|| self.invalid(&path, "missing required element <orderNumber>"))?,
})
}
fn tcc(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<ThermalCyclingConditions> {
let id = self.id_attr(e, "rdml/thermalCyclingConditions")?;
let path = format!("rdml/thermalCyclingConditions[{id}]");
let mut out = ThermalCyclingConditions::new(id);
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"description" => out.description = Some(self.content(cempty, &path)?),
b"documentation" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/documentation"))?;
out.documentation
.push(self.make_ref(raw, &format!("{path}/documentation"))?);
}
b"lidTemperature" => {
let t = self.content(cempty, &path)?;
out.lid_temperature =
Some(self.parse_f64(&t, &format!("{path}/lidTemperature"))?);
}
b"experimenter" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/experimenter"))?;
out.experimenters
.push(self.make_ref(raw, &format!("{path}/experimenter"))?);
}
b"step" => out.steps.push(self.step(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
fn step(&mut self, empty: bool, parent: &str) -> Result<Step> {
let path = format!("{parent}/step");
let mut nr = None;
let mut description = None;
let mut kind = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"nr" => {
let t = self.content(cempty, &path)?;
nr = Some(self.parse_u32_nonzero(&t, &format!("{path}/nr"))?);
}
b"description" => description = Some(self.content(cempty, &path)?),
b"temperature" => {
kind = Some(StepKind::Temperature(self.temperature_step(cempty, &path)?));
}
b"gradient" => {
kind = Some(StepKind::Gradient(self.gradient_step(cempty, &path)?));
}
b"loop" => kind = Some(StepKind::Loop(self.loop_step(cempty, &path)?)),
b"pause" => kind = Some(StepKind::Pause(self.pause_step(cempty, &path)?)),
b"lidOpen" => {
if !cempty {
self.r.read_to_end(c.name())?;
}
kind = Some(StepKind::LidOpen);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(Step {
nr: nr.ok_or_else(|| self.invalid(&path, "missing required element <nr>"))?,
description,
kind: kind.ok_or_else(|| {
self.invalid(
&path,
"missing step action (one of temperature/gradient/loop/pause/lidOpen)",
)
})?,
})
}
fn temperature_step(&mut self, empty: bool, parent: &str) -> Result<TemperatureStep> {
let path = format!("{parent}/temperature");
let mut temperature = None;
let mut duration = None;
let mut temperature_change = None;
let mut duration_change = None;
let mut measure = None;
let mut ramp = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"temperature" => {
let t = self.content(cempty, &path)?;
temperature = Some(self.parse_f64(&t, &format!("{path}/temperature"))?);
}
b"duration" => {
let t = self.content(cempty, &path)?;
duration = Some(self.parse_u32_nonzero(&t, &format!("{path}/duration"))?);
}
b"temperatureChange" => {
let t = self.content(cempty, &path)?;
temperature_change =
Some(self.parse_f64(&t, &format!("{path}/temperatureChange"))?);
}
b"durationChange" => {
let t = self.content(cempty, &path)?;
duration_change =
Some(self.parse_i32(&t, &format!("{path}/durationChange"))?);
}
b"measure" => {
let t = self.content(cempty, &path)?;
measure = self.parse_enum_lenient(&t, &format!("{path}/measure"));
}
b"ramp" => {
let t = self.content(cempty, &path)?;
ramp = Some(self.parse_f64(&t, &format!("{path}/ramp"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
let mut out = TemperatureStep::new(
temperature
.ok_or_else(|| self.invalid(&path, "missing required element <temperature>"))?,
duration.ok_or_else(|| self.invalid(&path, "missing required element <duration>"))?,
);
out.temperature_change = temperature_change;
out.duration_change = duration_change;
out.measure = measure;
out.ramp = ramp;
Ok(out)
}
fn gradient_step(&mut self, empty: bool, parent: &str) -> Result<GradientStep> {
let path = format!("{parent}/gradient");
let mut high = None;
let mut low = None;
let mut duration = None;
let mut temperature_change = None;
let mut duration_change = None;
let mut measure = None;
let mut ramp = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"highTemperature" => {
let t = self.content(cempty, &path)?;
high = Some(self.parse_f64(&t, &format!("{path}/highTemperature"))?);
}
b"lowTemperature" => {
let t = self.content(cempty, &path)?;
low = Some(self.parse_f64(&t, &format!("{path}/lowTemperature"))?);
}
b"duration" => {
let t = self.content(cempty, &path)?;
duration = Some(self.parse_u32_nonzero(&t, &format!("{path}/duration"))?);
}
b"temperatureChange" => {
let t = self.content(cempty, &path)?;
temperature_change =
Some(self.parse_f64(&t, &format!("{path}/temperatureChange"))?);
}
b"durationChange" => {
let t = self.content(cempty, &path)?;
duration_change =
Some(self.parse_i32(&t, &format!("{path}/durationChange"))?);
}
b"measure" => {
let t = self.content(cempty, &path)?;
measure = self.parse_enum_lenient(&t, &format!("{path}/measure"));
}
b"ramp" => {
let t = self.content(cempty, &path)?;
ramp = Some(self.parse_f64(&t, &format!("{path}/ramp"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
let mut out = GradientStep::new(
high.ok_or_else(|| self.invalid(&path, "missing required element <highTemperature>"))?,
low.ok_or_else(|| self.invalid(&path, "missing required element <lowTemperature>"))?,
duration.ok_or_else(|| self.invalid(&path, "missing required element <duration>"))?,
);
out.temperature_change = temperature_change;
out.duration_change = duration_change;
out.measure = measure;
out.ramp = ramp;
Ok(out)
}
fn loop_step(&mut self, empty: bool, parent: &str) -> Result<LoopStep> {
let path = format!("{parent}/loop");
let mut goto = None;
let mut repeat = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"goto" => {
let t = self.content(cempty, &path)?;
goto = Some(self.parse_u32_nonzero(&t, &format!("{path}/goto"))?);
}
b"repeat" => {
let t = self.content(cempty, &path)?;
repeat = Some(self.parse_u32(&t, &format!("{path}/repeat"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(LoopStep {
goto: goto.ok_or_else(|| self.invalid(&path, "missing required element <goto>"))?,
repeat: repeat
.ok_or_else(|| self.invalid(&path, "missing required element <repeat>"))?,
})
}
fn pause_step(&mut self, empty: bool, parent: &str) -> Result<PauseStep> {
let path = format!("{parent}/pause");
let mut temperature = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"temperature" => {
let t = self.content(cempty, &path)?;
temperature = Some(self.parse_f64(&t, &format!("{path}/temperature"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(PauseStep {
temperature: temperature
.ok_or_else(|| self.invalid(&path, "missing required element <temperature>"))?,
})
}
fn experiment(&mut self, e: &BytesStart<'_>, empty: bool) -> Result<Experiment> {
let id = self.id_attr(e, "rdml/experiment")?;
let path = format!("rdml/experiment[{id}]");
let mut out = Experiment::new(id);
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"description" => out.description = Some(self.content(cempty, &path)?),
b"documentation" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/documentation"))?;
out.documentation
.push(self.make_ref(raw, &format!("{path}/documentation"))?);
}
b"run" => {
let run = self.run(&c, cempty, &path)?;
out.runs.push(run);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(out)
}
#[allow(clippy::too_many_lines)]
fn run(&mut self, e: &BytesStart<'_>, empty: bool, parent: &str) -> Result<Run> {
let id = self.id_attr(e, &format!("{parent}/run"))?;
let path = format!("{parent}/run[{id}]");
let mut out = Run::new(id, PcrFormat::free_format());
let mut saw_format = false;
let mut legacy_react_ids: Vec<String> = Vec::new();
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"description" => out.description = Some(self.content(cempty, &path)?),
b"documentation" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/documentation"))?;
out.documentation
.push(self.make_ref(raw, &format!("{path}/documentation"))?);
}
b"experimenter" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/experimenter"))?;
out.experimenters
.push(self.make_ref(raw, &format!("{path}/experimenter"))?);
}
b"instrument" => out.instrument = Some(self.content(cempty, &path)?),
b"dataCollectionSoftware" => {
out.data_collection_software =
Some(self.data_collection_software(cempty, &path)?);
}
b"backgroundDeterminationMethod" => {
out.background_determination_method = Some(self.content(cempty, &path)?);
}
b"cqDetectionMethod" => {
let t = self.content(cempty, &path)?;
out.cq_detection_method =
self.parse_enum_lenient(&t, &format!("{path}/cqDetectionMethod"));
}
b"thermalCyclingConditions" => {
let raw =
self.ref_id(&c, cempty, &format!("{path}/thermalCyclingConditions"))?;
out.thermal_cycling_conditions =
Some(self.make_ref(raw, &format!("{path}/thermalCyclingConditions"))?);
}
b"pcrFormat" => {
out.pcr_format = self.pcr_format(cempty, &path)?;
saw_format = true;
}
b"runDate" => {
let t = self.content(cempty, &path)?;
out.run_date = Some(self.parse_datetime(&t, &format!("{path}/runDate"))?);
}
b"react" => {
let react = self.react(&c, cempty, &path, &mut legacy_react_ids)?;
out.reacts.push(react);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
if !saw_format {
self.note(
format!("{path}/pcrFormat"),
"missing required element <pcrFormat>; defaulted to free format \
(rows -1, columns 1)",
);
}
if self.version == RdmlVersion::V1_0 {
self.assign_legacy_react_ids(&mut out, &legacy_react_ids, &path);
}
Ok(out)
}
fn data_collection_software(
&mut self,
empty: bool,
parent: &str,
) -> Result<DataCollectionSoftware> {
let path = format!("{parent}/dataCollectionSoftware");
let mut sw_name = None;
let mut sw_version = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"name" => sw_name = Some(self.content(cempty, &path)?),
b"version" => sw_version = Some(self.content(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(DataCollectionSoftware {
name: sw_name.ok_or_else(|| self.invalid(&path, "missing required element <name>"))?,
version: sw_version
.ok_or_else(|| self.invalid(&path, "missing required element <version>"))?,
})
}
fn pcr_format(&mut self, empty: bool, parent: &str) -> Result<PcrFormat> {
let path = format!("{parent}/pcrFormat");
if self.version == RdmlVersion::V1_0 {
let text = self.content(empty, &path)?;
return Ok(self.map_legacy_format(&text, &path));
}
let mut rows = None;
let mut columns = None;
let mut row_label = None;
let mut column_label = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"rows" => {
let t = self.content(cempty, &path)?;
rows = Some(self.parse_i32(&t, &format!("{path}/rows"))?);
}
b"columns" => {
let t = self.content(cempty, &path)?;
columns = Some(self.parse_i32(&t, &format!("{path}/columns"))?);
}
b"rowLabel" => {
let t = self.content(cempty, &path)?;
row_label = Some(self.parse_enum(&t, &format!("{path}/rowLabel"))?);
}
b"columnLabel" => {
let t = self.content(cempty, &path)?;
column_label = Some(self.parse_enum(&t, &format!("{path}/columnLabel"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(PcrFormat {
rows: rows.ok_or_else(|| self.invalid(&path, "missing required element <rows>"))?,
columns: columns
.ok_or_else(|| self.invalid(&path, "missing required element <columns>"))?,
row_label: row_label
.ok_or_else(|| self.invalid(&path, "missing required element <rowLabel>"))?,
column_label: column_label
.ok_or_else(|| self.invalid(&path, "missing required element <columnLabel>"))?,
})
}
fn map_legacy_format(&mut self, text: &str, path: &str) -> PcrFormat {
let format = match text.trim() {
"single-well; 1" => PcrFormat::single_well(),
"48-well plate; A1-F8" => PcrFormat::plate48(),
"96-well plate; A1-H12" => PcrFormat::plate96(),
"384-well plate; A1-P24" => PcrFormat::plate384(),
"3072-well plate; A1a1-D12h8" => PcrFormat::array3072(),
"32-well rotor; 1-32" => PcrFormat::rotor(32),
"72-well rotor; 1-72" => PcrFormat::rotor(72),
"100-well rotor; 1-100" => PcrFormat::rotor(100),
"free format" => PcrFormat::free_format(),
other => {
self.note(
path,
format!("unknown RDML 1.0 pcrFormat `{other}`; treated as free format"),
);
return PcrFormat::free_format();
}
};
self.note(
path,
format!(
"RDML 1.0 pcrFormat `{}` mapped to rows {} × columns {}",
text.trim(),
format.rows,
format.columns
),
);
format
}
fn react(
&mut self,
e: &BytesStart<'_>,
empty: bool,
parent: &str,
legacy_react_ids: &mut Vec<String>,
) -> Result<React> {
let raw_id = self
.attr(e, b"id", &format!("{parent}/react"))?
.ok_or_else(|| {
self.invalid(
&format!("{parent}/react"),
"missing required attribute `id`",
)
})?;
let (id, path) = if self.version == RdmlVersion::V1_0 {
legacy_react_ids.push(raw_id.clone());
let placeholder = u32::try_from(legacy_react_ids.len())
.ok()
.and_then(NonZeroU32::new)
.unwrap_or(NonZeroU32::MAX);
(placeholder, format!("{parent}/react[{raw_id}]"))
} else {
let path = format!("{parent}/react[{raw_id}]");
(self.parse_u32_nonzero(&raw_id, &path)?, path)
};
let mut sample = None;
let mut vol = None;
let mut data = Vec::new();
let mut partitions = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"sample" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/sample"))?;
sample = Some(self.make_ref(raw, &format!("{path}/sample"))?);
}
b"vol" => {
let t = self.content(cempty, &path)?;
vol = Some(self.parse_f64(&t, &format!("{path}/vol"))?);
}
b"data" => data.push(self.data(cempty, &path)?),
b"partitions" => partitions = Some(self.partitions(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
let mut out = React::new(
id,
sample.ok_or_else(|| self.invalid(&path, "missing required element <sample>"))?,
);
out.vol = vol;
out.data = data;
out.partitions = partitions;
Ok(out)
}
fn assign_legacy_react_ids(&mut self, run: &mut Run, raw_ids: &[String], path: &str) {
use std::collections::HashSet;
let mut used: HashSet<u32> = HashSet::new();
let mut mapped = 0usize;
let mut resolved: Vec<Option<NonZeroU32>> = Vec::with_capacity(raw_ids.len());
for raw in raw_ids {
let id = parse_well_label(raw, &run.pcr_format).filter(|id| used.insert(id.get()));
if id.is_some() {
mapped += 1;
}
resolved.push(id);
}
let mut next = 1u32;
for (raw, slot) in raw_ids.iter().zip(resolved.iter_mut()) {
if slot.is_none() {
while used.contains(&next) {
next += 1;
}
used.insert(next);
*slot = NonZeroU32::new(next);
self.note(
format!("{path}/react[{raw}]"),
format!(
"RDML 1.0 react id `{raw}` could not be mapped to a plate position; \
assigned lowest unused id {next}"
),
);
}
}
for (react, slot) in run.reacts.iter_mut().zip(resolved) {
react.id = slot.expect("all slots filled by the two passes");
}
if mapped > 0 {
self.note(
format!("{path}/react"),
format!(
"mapped {mapped} RDML 1.0 react well labels to positional ids \
(row-first/column-second)"
),
);
}
}
fn data(&mut self, empty: bool, parent: &str) -> Result<Data> {
let path = format!("{parent}/data");
let mut tar = None;
let mut out = Data::new(TargetRef::new("placeholder").expect("non-empty"));
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"tar" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/tar"))?;
tar = Some(self.make_ref::<TargetRef>(raw, &format!("{path}/tar"))?);
}
b"cq" => {
let t = self.content(cempty, &path)?;
out.cq = self.sentinel_f64(&t, &format!("{path}/cq"))?;
}
b"N0" => {
let t = self.content(cempty, &path)?;
out.n0 = self.sentinel_f64(&t, &format!("{path}/N0"))?;
}
b"Ncopy" => {
let t = self.content(cempty, &path)?;
out.n_copy = self.sentinel_f64(&t, &format!("{path}/Ncopy"))?;
}
b"ampEffMet" => out.amp_eff_met = Some(self.content(cempty, &path)?),
b"ampEff" => {
let t = self.content(cempty, &path)?;
out.amp_eff = Some(self.parse_f64(&t, &format!("{path}/ampEff"))?);
}
b"ampEffSE" => {
let t = self.content(cempty, &path)?;
out.amp_eff_se = Some(self.parse_f64(&t, &format!("{path}/ampEffSE"))?);
}
b"corrF" => {
let t = self.content(cempty, &path)?;
out.corr_f = Some(self.parse_f64(&t, &format!("{path}/corrF"))?);
}
b"corrP" => {
let t = self.content(cempty, &path)?;
out.corr_p = self.sentinel_f64(&t, &format!("{path}/corrP"))?;
}
b"corrCq" => {
let t = self.content(cempty, &path)?;
out.corr_cq = self.sentinel_f64(&t, &format!("{path}/corrCq"))?;
}
b"meltTemp" => {
let t = self.content(cempty, &path)?;
out.melt_temp = Some(self.parse_f64(&t, &format!("{path}/meltTemp"))?);
}
b"excl" => {
let t = self.content(cempty, &path)?;
out.excl = Some(Reasons::from_joined(&t));
}
b"note" => {
let t = self.content(cempty, &path)?;
out.note = Some(Reasons::from_joined(&t));
}
b"adp" => out.adps.push(self.adp(cempty, &path)?),
b"mdp" => out.mdps.push(self.mdp(cempty, &path)?),
b"endPt" => {
let t = self.content(cempty, &path)?;
out.end_pt = Some(self.parse_f64(&t, &format!("{path}/endPt"))?);
}
b"bgFluor" => {
let t = self.content(cempty, &path)?;
out.bg_fluor = Some(self.parse_f64(&t, &format!("{path}/bgFluor"))?);
}
b"bgFluorSlp" => {
let t = self.content(cempty, &path)?;
out.bg_fluor_slp = Some(self.parse_f64(&t, &format!("{path}/bgFluorSlp"))?);
}
b"quantFluor" => {
let t = self.content(cempty, &path)?;
out.quant_fluor = Some(self.parse_f64(&t, &format!("{path}/quantFluor"))?);
}
b"quantity" if self.version == RdmlVersion::V1_0 => {
let qpath = format!("{path}/quantity");
let (value, unit) = self.quantity_body(cempty, &qpath)?;
self.note(
qpath,
format!(
"dropped RDML 1.0 data quantity ({value} {unit}); the element \
was removed in 1.1 — calculated quantities belong to analysis \
software"
),
);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
out.tar = tar.ok_or_else(|| self.invalid(&path, "missing required element <tar>"))?;
Ok(out)
}
fn adp(&mut self, empty: bool, parent: &str) -> Result<AmpPoint> {
let path = format!("{parent}/adp");
let mut cyc = None;
let mut tmp = None;
let mut fluor = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"cyc" => {
let t = self.content(cempty, &path)?;
cyc = Some(self.parse_f64(&t, &format!("{path}/cyc"))?);
}
b"tmp" => {
let t = self.content(cempty, &path)?;
tmp = Some(self.parse_f64(&t, &format!("{path}/tmp"))?);
}
b"fluor" => {
let t = self.content(cempty, &path)?;
fluor = Some(self.parse_f64(&t, &format!("{path}/fluor"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(AmpPoint {
cyc: cyc.ok_or_else(|| self.invalid(&path, "missing required element <cyc>"))?,
tmp,
fluor: fluor.ok_or_else(|| self.invalid(&path, "missing required element <fluor>"))?,
})
}
fn mdp(&mut self, empty: bool, parent: &str) -> Result<MeltPoint> {
let path = format!("{parent}/mdp");
let mut tmp = None;
let mut fluor = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"tmp" => {
let t = self.content(cempty, &path)?;
tmp = Some(self.parse_f64(&t, &format!("{path}/tmp"))?);
}
b"fluor" => {
let t = self.content(cempty, &path)?;
fluor = Some(self.parse_f64(&t, &format!("{path}/fluor"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(MeltPoint {
tmp: tmp.ok_or_else(|| self.invalid(&path, "missing required element <tmp>"))?,
fluor: fluor.ok_or_else(|| self.invalid(&path, "missing required element <fluor>"))?,
})
}
fn partitions(&mut self, empty: bool, parent: &str) -> Result<Partitions> {
let path = format!("{parent}/partitions");
let mut volume = None;
let mut end_pt_table = None;
let mut data = Vec::new();
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"volume" => {
let t = self.content(cempty, &path)?;
volume = Some(self.parse_f64(&t, &format!("{path}/volume"))?);
}
b"endPtTable" => end_pt_table = Some(self.content(cempty, &path)?),
b"data" => data.push(self.partition_data(cempty, &path)?),
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(Partitions {
volume: volume
.ok_or_else(|| self.invalid(&path, "missing required element <volume>"))?,
end_pt_table,
data,
})
}
fn partition_data(&mut self, empty: bool, parent: &str) -> Result<PartitionData> {
let path = format!("{parent}/data");
let mut tar = None;
let mut excluded = None;
let mut note = None;
let mut pos = None;
let mut neg = None;
let mut undef = None;
let mut excl = None;
let mut conc = None;
if !empty {
loop {
let (c, cempty) = match self.r.read_event()? {
Event::Start(c) => (c.into_owned(), false),
Event::Empty(c) => (c.into_owned(), true),
Event::End(_) => break,
Event::Eof => return Err(self.invalid(&path, "unexpected end of document")),
_ => continue,
};
match c.local_name().as_ref() {
b"tar" => {
let raw = self.ref_id(&c, cempty, &format!("{path}/tar"))?;
tar = Some(self.make_ref::<TargetRef>(raw, &format!("{path}/tar"))?);
}
b"excluded" => {
let t = self.content(cempty, &path)?;
excluded = Some(Reasons::from_joined(&t));
}
b"note" => {
let t = self.content(cempty, &path)?;
note = Some(Reasons::from_joined(&t));
}
b"pos" => {
let t = self.content(cempty, &path)?;
pos = Some(self.parse_i32(&t, &format!("{path}/pos"))?);
}
b"neg" => {
let t = self.content(cempty, &path)?;
neg = Some(self.parse_i32(&t, &format!("{path}/neg"))?);
}
b"undef" => {
let t = self.content(cempty, &path)?;
undef = Some(self.parse_i32(&t, &format!("{path}/undef"))?);
}
b"excl" => {
let t = self.content(cempty, &path)?;
excl = Some(self.parse_i32(&t, &format!("{path}/excl"))?);
}
b"conc" => {
let t = self.content(cempty, &path)?;
conc = Some(self.parse_f64(&t, &format!("{path}/conc"))?);
}
_ => self.skip_unknown(&c, cempty, &path)?,
}
}
}
Ok(PartitionData {
tar: tar.ok_or_else(|| self.invalid(&path, "missing required element <tar>"))?,
excluded,
note,
pos: pos.ok_or_else(|| self.invalid(&path, "missing required element <pos>"))?,
neg: neg.ok_or_else(|| self.invalid(&path, "missing required element <neg>"))?,
undef,
excl,
conc,
})
}
}
enum LegacyTemplate {
Quantity(f64, QuantityUnit),
Quality { method: String, result: f64 },
}
enum Slot {
Forward,
Reverse,
Probe1,
Probe2,
Amplicon,
}
impl Slot {
fn name(&self) -> &'static str {
match self {
Slot::Forward => "forwardPrimer",
Slot::Reverse => "reversePrimer",
Slot::Probe1 => "probe1",
Slot::Probe2 => "probe2",
Slot::Amplicon => "amplicon",
}
}
}
fn parse_well_label(label: &str, format: &PcrFormat) -> Option<NonZeroU32> {
let label = label.trim();
if let Ok(n) = label.parse::<NonZeroU32>() {
return Some(n);
}
let columns = u32::try_from(format.columns).ok().filter(|&c| c > 0)?;
let mut chars = label.chars().peekable();
let mut take_while = |pred: fn(char) -> bool| {
let mut s = String::new();
while let Some(&c) = chars.peek() {
if pred(c) {
s.push(c);
chars.next();
} else {
break;
}
}
s
};
let outer_row = take_while(|c| c.is_ascii_uppercase());
let outer_col = take_while(|c| c.is_ascii_digit());
let inner_row = take_while(|c| c.is_ascii_lowercase());
let inner_col = take_while(|c| c.is_ascii_digit());
if outer_row.is_empty() || outer_col.is_empty() || !take_while(|_| true).is_empty() {
return None;
}
let row_from_letters = |letters: &str, lowercase: bool| -> Option<u32> {
let mut row = 0u32;
for c in letters.chars() {
let base = if lowercase { 'a' } else { 'A' };
row = row * 26 + (c as u32 - base as u32 + 1);
}
Some(row)
};
let outer_row = row_from_letters(&outer_row, false)?;
let outer_col: u32 = outer_col.parse().ok()?;
if inner_row.is_empty() {
if inner_col.is_empty() && outer_col >= 1 && outer_col <= columns {
return NonZeroU32::new((outer_row - 1) * columns + outer_col);
}
return None;
}
let inner_row = row_from_letters(&inner_row, true)?;
let inner_col: u32 = inner_col.parse().ok()?;
if !(1..=8).contains(&inner_row) || !(1..=8).contains(&inner_col) {
return None;
}
let row = (outer_row - 1) * 8 + inner_row;
let col = (outer_col - 1) * 8 + inner_col;
if col > columns {
return None;
}
NonZeroU32::new((row - 1) * columns + col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn well_labels() {
let plate96 = PcrFormat::plate96();
assert_eq!(parse_well_label("A1", &plate96).unwrap().get(), 1);
assert_eq!(parse_well_label("B3", &plate96).unwrap().get(), 15);
assert_eq!(parse_well_label("H12", &plate96).unwrap().get(), 96);
assert_eq!(parse_well_label("17", &plate96).unwrap().get(), 17);
assert!(parse_well_label("H13", &plate96).is_none());
assert!(parse_well_label("wobble", &plate96).is_none());
let array = PcrFormat::array3072();
assert_eq!(parse_well_label("A1a1", &array).unwrap().get(), 1);
assert_eq!(parse_well_label("A1a2", &array).unwrap().get(), 2);
assert_eq!(parse_well_label("A2a1", &array).unwrap().get(), 9);
assert_eq!(parse_well_label("A1b1", &array).unwrap().get(), 97);
assert_eq!(parse_well_label("B1a1", &array).unwrap().get(), 769);
assert_eq!(parse_well_label("D12h8", &array).unwrap().get(), 3072);
}
}