use crate::error::{Error, Result};
use quick_xml::events::Event;
use quick_xml::reader::Reader;
fn local_name(name: &[u8]) -> &[u8] {
name.split(|b| *b == b':').next_back().unwrap_or(name)
}
pub(crate) fn extract_record_metadata(xml: &str) -> Result<Vec<Option<String>>> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut skip_buf = Vec::new();
let mut stack: Vec<Vec<u8>> = Vec::new();
let mut payload_depth: Option<usize> = None;
let mut record_depth: Option<usize> = None;
let mut current_record: Option<usize> = None;
let mut results: Vec<Option<String>> = Vec::new();
loop {
let event = match reader.read_event_into(&mut buf) {
Ok(event) => event,
Err(err) => return Err(Error::XmlParse(err.into())),
};
match event {
Event::Start(e) => {
let name = local_name(e.name().into_inner()).to_vec();
stack.push(name.clone());
match name.as_slice() {
b"GetRecord" | b"ListRecords" if payload_depth.is_none() => {
payload_depth = Some(stack.len());
}
b"record" if record_depth.is_none() => match payload_depth {
Some(pd) if stack.len() == pd + 1 => {
record_depth = Some(stack.len());
let idx = results.len();
results.push(None);
current_record = Some(idx);
}
_ => (),
},
b"metadata" => {
match (current_record, record_depth) {
(Some(idx), Some(rd)) if stack.len() == rd + 1 => {
let span = reader
.read_to_end_into(e.name(), &mut skip_buf)
.map_err(|err| Error::XmlParse(err.into()))?;
let start = span.start as usize;
let end = span.end as usize;
let inner_xml = xml.get(start..end).unwrap_or_default().to_string();
results[idx] = Some(inner_xml);
stack.pop();
}
_ => (),
}
}
_ => (),
}
}
Event::Empty(e) => {
let name = local_name(e.name().into_inner());
match (name, record_depth, current_record) {
(b"metadata", Some(rd), Some(idx)) if stack.len() == rd => {
results[idx] = Some(String::new());
}
_ => (),
}
}
Event::End(e) => {
let name = local_name(e.name().into_inner());
match (name, payload_depth) {
(b"GetRecord" | b"ListRecords", Some(pd)) if stack.len() == pd => {
payload_depth = None;
}
_ => (),
}
match (name, record_depth) {
(b"record", Some(rd)) if stack.len() == rd => {
record_depth = None;
current_record = None;
}
_ => (),
}
stack.pop();
}
Event::Eof => break,
_ => (),
}
buf.clear();
}
Ok(results)
}
pub fn extract_metadata(xml: &str) -> Vec<String> {
extract_record_metadata(xml)
.ok()
.into_iter()
.flatten()
.flatten()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn test_extract_metadata_single_record() {
let xml =
fs::read_to_string("tests/fixtures/get_record.xml").expect("Failed to load fixture");
let results = extract_metadata(&xml);
assert_eq!(results.len(), 1);
assert!(results[0].contains("<ead"));
assert!(results[0].contains("xmlns=\"urn:isbn:1-931666-22-9\""));
}
#[test]
fn test_extract_metadata_list_records() {
let xml =
fs::read_to_string("tests/fixtures/list_records.xml").expect("Failed to load fixture");
let results = extract_metadata(&xml);
assert!(results.len() > 1);
assert!(results[0].contains("<oai_dc:dc"));
assert!(results[0].contains("xmlns:dc=\"http://purl.org/dc/elements/1.1/\""));
for metadata in &results {
assert!(!metadata.is_empty());
}
}
#[test]
fn test_extract_metadata_with_newlines() {
let xml = r#"<OAI-PMH><ListRecords><record><header></header><metadata>
<ead xmlns="test">
<content>here</content>
</ead>
</metadata></record></ListRecords></OAI-PMH>"#;
let results = extract_metadata(xml);
assert_eq!(results.len(), 1);
assert!(results[0].contains("<ead"));
assert!(results[0].contains("</ead>"));
}
#[test]
fn test_extract_metadata_multiple_records() {
let xml = r#"
<OAI-PMH>
<ListRecords>
<record>
<header><identifier>id1</identifier></header>
<metadata><dc xmlns="http://purl.org/dc/elements/1.1/"><title>First</title></dc></metadata>
</record>
<record>
<header><identifier>id2</identifier></header>
<metadata><dc xmlns="http://purl.org/dc/elements/1.1/"><title>Second</title></dc></metadata>
</record>
</ListRecords>
</OAI-PMH>"#;
let results = extract_metadata(xml);
assert_eq!(results.len(), 2);
assert!(results[0].contains("First"));
assert!(results[1].contains("Second"));
}
#[test]
fn test_extract_record_metadata_alignment_with_deleted() {
let xml = fs::read_to_string("tests/fixtures/list_records_with_deleted.xml")
.expect("Failed to load fixture");
let results = extract_record_metadata(&xml).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0], None);
let second = results[1].as_ref().unwrap();
assert!(second.contains("<oai_dc:dc"));
assert!(second.contains("<dc:title>Alive</dc:title>"));
}
}