use std::io::BufRead;
use anyhow::{Context, Result};
use quick_xml::Reader;
use quick_xml::events::Event;
pub struct Records<R: BufRead, T> {
reader: Reader<R>,
buffer: Vec<u8>,
text: String,
marker: std::marker::PhantomData<T>,
}
pub trait Record: Sized {
const ELEMENT: &'static str;
fn open(attributes: Attributes<'_>) -> Self;
fn field(&mut self, path: &[&str], text: &str, attributes: Attributes<'_>);
}
pub struct Attributes<'a> {
pairs: &'a [(String, String)],
}
impl Attributes<'_> {
#[must_use]
pub fn get(&self, name: &str) -> Option<&str> {
self.pairs.iter().find(|(key, _)| key == name).map(|(_, value)| value.as_str())
}
pub fn parse<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
self.get(name)?.parse().ok()
}
}
impl<R: BufRead, T: Record> Records<R, T> {
pub fn new(input: R) -> Self {
let mut reader = Reader::from_reader(input);
reader.config_mut().check_end_names = false;
reader.config_mut().trim_text(false);
Self {
reader,
buffer: Vec::with_capacity(1 << 16),
text: String::new(),
marker: std::marker::PhantomData,
}
}
pub fn next_record(&mut self) -> Result<Option<T>> {
loop {
self.buffer.clear();
let event = match self.reader.read_event_into(&mut self.buffer) {
Ok(event) => event,
Err(quick_xml::Error::Syntax(_)) => return Ok(None),
Err(error) => return Err(error).context("failed to read the dump"),
};
match event {
Event::Eof => return Ok(None),
Event::Start(start) if start.name().as_ref() == T::ELEMENT.as_bytes() => {
let attributes = collect_attributes(&start);
let mut record = T::open(Attributes { pairs: &attributes });
self.read_subtree(&mut record)?;
return Ok(Some(record));
}
Event::Empty(empty) if empty.name().as_ref() == T::ELEMENT.as_bytes() => {
let attributes = collect_attributes(&empty);
return Ok(Some(T::open(Attributes { pairs: &attributes })));
}
_ => {}
}
}
}
fn read_subtree(&mut self, record: &mut T) -> Result<()> {
let mut path: Vec<String> = Vec::with_capacity(4);
let mut open_attributes: Vec<Vec<(String, String)>> = Vec::with_capacity(4);
let mut depth_reported = 0usize;
self.text.clear();
loop {
self.buffer.clear();
let event = match self.reader.read_event_into(&mut self.buffer) {
Ok(event) => event,
Err(quick_xml::Error::Syntax(_)) => return Ok(()),
Err(error) => return Err(error).context("failed to read the dump"),
};
match event {
Event::Start(start) => {
path.push(String::from_utf8_lossy(start.name().as_ref()).into_owned());
open_attributes.push(collect_attributes(&start));
self.text.clear();
}
Event::Empty(empty) => {
let name = String::from_utf8_lossy(empty.name().as_ref()).into_owned();
path.push(name);
let attributes = collect_attributes(&empty);
report(record, &path, "", &attributes);
depth_reported = path.len();
path.pop();
}
Event::Text(text) => {
if let Ok(decoded) = text.decode() {
self.text.push_str(&decoded);
}
}
Event::GeneralRef(reference) => {
if let Ok(name) = reference.decode() {
self.text.push_str(&resolve_entity(&name));
}
}
Event::CData(data) => {
self.text.push_str(&String::from_utf8_lossy(&data));
}
Event::End(end) => {
if end.name().as_ref() == T::ELEMENT.as_bytes() && path.is_empty() {
return Ok(());
}
let attributes = open_attributes.pop().unwrap_or_default();
if !path.is_empty() {
let is_container = depth_reported > path.len();
if !is_container {
report(record, &path, self.text.trim(), &attributes);
}
path.pop();
depth_reported = path.len() + 1;
}
self.text.clear();
}
Event::Eof => return Ok(()),
_ => {}
}
}
}
}
fn resolve_entity(name: &str) -> String {
if let Some(digits) = name.strip_prefix("#x").or_else(|| name.strip_prefix("#X")) {
return u32::from_str_radix(digits, 16)
.ok()
.and_then(char::from_u32)
.map_or_else(|| format!("&{name};"), String::from);
}
if let Some(digits) = name.strip_prefix('#') {
return digits
.parse::<u32>()
.ok()
.and_then(char::from_u32)
.map_or_else(|| format!("&{name};"), String::from);
}
match name {
"amp" => "&".to_string(),
"lt" => "<".to_string(),
"gt" => ">".to_string(),
"quot" => "\"".to_string(),
"apos" => "'".to_string(),
other => format!("&{other};"),
}
}
fn collect_attributes(start: &quick_xml::events::BytesStart<'_>) -> Vec<(String, String)> {
start
.attributes()
.with_checks(false)
.filter_map(Result::ok)
.map(|attribute| {
(
String::from_utf8_lossy(attribute.key.as_ref()).into_owned(),
String::from_utf8_lossy(&attribute.value).into_owned(),
)
})
.collect()
}
fn report<T: Record>(record: &mut T, path: &[String], text: &str, attributes: &[(String, String)]) {
let borrowed: Vec<&str> = path.iter().map(String::as_str).collect();
record.field(&borrowed, text, Attributes { pairs: attributes });
}
#[cfg(test)]
mod tests {
use super::*;
struct Spy {
fields: Vec<(String, String, Option<String>)>,
}
impl Record for Spy {
const ELEMENT: &'static str = "artist";
fn open(_: Attributes<'_>) -> Self {
Self { fields: Vec::new() }
}
fn field(&mut self, path: &[&str], text: &str, attributes: Attributes<'_>) {
self.fields.push((path.join("/"), text.to_string(), attributes.get("id").map(str::to_string)));
}
}
fn read(xml: &str) -> Vec<Spy> {
let mut records = Records::<_, Spy>::new(xml.as_bytes());
let mut out = Vec::new();
while let Some(record) = records.next_record().unwrap() {
out.push(record);
}
out
}
fn paths(record: &Spy) -> Vec<(&str, &str)> {
record.fields.iter().map(|(p, t, _)| (p.as_str(), t.as_str())).collect()
}
#[test]
fn reads_child_elements_with_their_path() {
let records = read("<artists><artist><id>1</id><name>The Persuader</name></artist></artists>");
assert_eq!(records.len(), 1);
assert_eq!(paths(&records[0]), vec![("id", "1"), ("name", "The Persuader")]);
}
#[test]
fn distinguishes_genres_from_styles_by_path() {
let records = read("<artists><artist><genres><genre>Electronic</genre></genres><styles><style>Techno</style></styles></artist></artists>");
assert_eq!(paths(&records[0]), vec![("genres/genre", "Electronic"), ("styles/style", "Techno")]);
}
#[test]
fn keeps_member_ids_distinct_from_the_records_own_id() {
let records = read("<artists><artist><id>2</id><members><id>26</id><name id=\"26\">Alexi Delano</name></members></artist></artists>");
assert_eq!(paths(&records[0]), vec![("id", "2"), ("members/id", "26"), ("members/name", "Alexi Delano")]);
}
#[test]
fn reads_the_id_attribute_on_nested_names() {
let records = read("<artists><artist><aliases><name id=\"239\">Jesper Dahlbäck</name></aliases></artist></artists>");
let alias = records[0].fields.iter().find(|(p, _, _)| p == "aliases/name").unwrap();
assert_eq!(alias.2.as_deref(), Some("239"));
assert_eq!(alias.1, "Jesper Dahlbäck");
}
#[test]
fn reads_an_id_carried_as_an_attribute_on_the_record() {
struct Master {
id: Option<i32>,
}
impl Record for Master {
const ELEMENT: &'static str = "master";
fn open(attributes: Attributes<'_>) -> Self {
Self { id: attributes.parse("id") }
}
fn field(&mut self, _: &[&str], _: &str, _: Attributes<'_>) {}
}
let mut records = Records::<_, Master>::new("<masters><master id=\"18500\"><title>New Soil</title></master></masters>".as_bytes());
let record = records.next_record().unwrap().unwrap();
assert_eq!(record.id, Some(18500));
}
#[test]
fn yields_every_record_in_sequence() {
let records = read("<artists><artist><id>1</id></artist><artist><id>2</id></artist><artist><id>3</id></artist></artists>");
let ids: Vec<&str> = records.iter().map(|r| r.fields[0].1.as_str()).collect();
assert_eq!(ids, vec!["1", "2", "3"]);
}
#[test]
fn preserves_non_ascii_and_entities() {
let records = read("<artists><artist><name>Sigur Rós</name><realname>AC&DC</realname></artist></artists>");
assert_eq!(paths(&records[0]), vec![("name", "Sigur Rós"), ("realname", "AC&DC")]);
}
#[test]
fn survives_an_empty_record() {
let records = read("<artists><artist/><artist><id>2</id></artist></artists>");
assert_eq!(records.len(), 2);
assert!(records[0].fields.is_empty());
}
#[test]
fn survives_a_truncated_document() {
let records = read("<artists><artist><id>1</id></artist><artist><id>2</id");
assert_eq!(records.len(), 2);
assert_eq!(paths(&records[0]), vec![("id", "1")]);
}
#[test]
fn reports_self_closing_elements_without_text() {
let records = read("<artists><artist><images><image height=\"450\" id=\"7\" uri=\"\"/></images></artist></artists>");
let image = records[0].fields.iter().find(|(p, _, _)| p == "images/image").unwrap();
assert_eq!(image.1, "");
assert_eq!(image.2.as_deref(), Some("7"));
}
#[test]
fn keeps_nested_paths_separate_across_siblings() {
let records =
read("<artists><artist><parentLabel id=\"4711\">Goldhead</parentLabel><sublabels><label id=\"2437\">Birdy</label></sublabels></artist></artists>");
assert_eq!(paths(&records[0]), vec![("parentLabel", "Goldhead"), ("sublabels/label", "Birdy")]);
}
}