use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceInfo {
pub name: String,
pub model: String,
}
pub fn parse_devices_xml(path: &Path) -> Option<DeviceInfo> {
let text = std::fs::read_to_string(path).ok()?;
let doc = roxmltree::Document::parse(&text).ok()?;
let device = doc
.descendants()
.find(|n| n.is_element() && n.tag_name().name() == "Device")?;
let name = child_text(device, "Name")?;
let model = child_text(device, "ModelNumber")?;
if name.is_empty() || model.is_empty() {
return None;
}
Some(DeviceInfo { name, model })
}
pub fn parse_acquired_time(path: &Path) -> Option<String> {
let text = std::fs::read_to_string(path).ok()?;
let doc = roxmltree::Document::parse(&text).ok()?;
let root = doc.root_element();
let value = child_text(root, "AcquiredTime")?;
if value.is_empty() {
None
} else {
Some(value)
}
}
fn child_text(node: roxmltree::Node, tag: &str) -> Option<String> {
node.children()
.find(|n| n.is_element() && n.tag_name().name() == tag)
.and_then(|n| n.text())
.map(|t| t.trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_temp(name: &str, contents: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"openaraw-metadata-test-{}-{}",
std::process::id(),
name
));
let mut f = std::fs::File::create(&path).expect("create temp file");
f.write_all(contents.as_bytes()).expect("write temp file");
path
}
#[test]
fn parses_first_device_as_qtof() {
let path = write_temp(
"devices-qtof.xml",
r#"<?xml version="1.0" encoding="utf-8"?>
<Devices>
<Version>1</Version>
<Device DeviceID="1">
<Name>QTOF</Name>
<ModelNumber>G6550A</ModelNumber>
<Type>6</Type>
</Device>
<Device DeviceID="2">
<Name>LowflowPump</Name>
<ModelNumber>G1376A</ModelNumber>
<Type>35</Type>
</Device>
</Devices>"#,
);
let info = parse_devices_xml(&path).expect("should parse");
assert_eq!(info.name, "QTOF");
assert_eq!(info.model, "G6550A");
let _ = std::fs::remove_file(&path);
}
#[test]
fn parses_first_device_as_qqq() {
let path = write_temp(
"devices-qqq.xml",
r#"<?xml version="1.0" encoding="UTF-8"?>
<Devices>
<Device DeviceID="1">
<Name>TandemQuadrupole</Name>
<ModelNumber>G6410A</ModelNumber>
<Type>5</Type>
</Device>
</Devices>"#,
);
let info = parse_devices_xml(&path).expect("should parse");
assert_eq!(info.name, "TandemQuadrupole");
assert_eq!(info.model, "G6410A");
let _ = std::fs::remove_file(&path);
}
#[test]
fn missing_devices_file_returns_none() {
let path = Path::new("/nonexistent/Devices.xml");
assert_eq!(parse_devices_xml(path), None);
}
#[test]
fn parses_acquired_time_with_z_suffix() {
let path = write_temp(
"contents-z.xml",
r#"<?xml version="1.0" encoding="UTF-8"?>
<Contents>
<Version>3</Version>
<AcquiredTime>2014-08-06T22:49:56Z</AcquiredTime>
</Contents>"#,
);
assert_eq!(
parse_acquired_time(&path).as_deref(),
Some("2014-08-06T22:49:56Z")
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn parses_acquired_time_with_offset_and_fractional_seconds() {
let path = write_temp(
"contents-offset.xml",
r#"<?xml version="1.0"?>
<Contents>
<AcquiredTime>2021-11-30T18:36:39.6755756-07:00</AcquiredTime>
</Contents>"#,
);
assert_eq!(
parse_acquired_time(&path).as_deref(),
Some("2021-11-30T18:36:39.6755756-07:00")
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn missing_contents_file_returns_none() {
let path = Path::new("/nonexistent/Contents.xml");
assert_eq!(parse_acquired_time(path), None);
}
#[test]
fn empty_acquired_time_returns_none() {
let path = write_temp(
"contents-empty.xml",
r#"<Contents><AcquiredTime></AcquiredTime></Contents>"#,
);
assert_eq!(parse_acquired_time(&path), None);
let _ = std::fs::remove_file(&path);
}
}