use anyhow::{Context, Result, bail};
use crate::{ExtractionOptions, QueryValue, TypeInferenceOptions, XmlMode};
use roxmltree::{Document, Node};
use super::{SheetData, parse_scalar_value};
pub(super) fn load_xml_sheet(
xml_path: &std::path::Path,
requested_sheet: Option<&str>,
inference_options: &TypeInferenceOptions,
extraction_options: &ExtractionOptions,
) -> Result<SheetData> {
let xml_content = std::fs::read_to_string(xml_path)
.with_context(|| format!("failed to read {}", xml_path.display()))?;
load_xml_sheet_from_str(
xml_path,
&xml_content,
requested_sheet,
inference_options,
extraction_options,
)
}
pub(super) fn load_xml_sheet_from_str(
xml_path: &std::path::Path,
xml_content: &str,
requested_sheet: Option<&str>,
inference_options: &TypeInferenceOptions,
extraction_options: &ExtractionOptions,
) -> Result<SheetData> {
let document = Document::parse(&xml_content)
.with_context(|| format!("failed to parse XML {}", xml_path.display()))?;
let root = document.root_element();
let scope = if let Some(sheet_tag) = requested_sheet {
root.descendants()
.find(|node| node.is_element() && node.tag_name().name() == sheet_tag)
.ok_or_else(|| anyhow::anyhow!("XML tag '{sheet_tag}' not found"))?
} else {
root
};
let records = match extraction_options.xml_mode {
XmlMode::Rows => {
let mut records = collect_xml_records(scope, inference_options);
if records.is_empty() {
let fallback = xml_row_from_children(scope, inference_options);
if fallback.is_empty() {
let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
bail!("XML scope '{scope_name}' does not contain tabular data");
}
records.push(fallback);
}
records
}
XmlMode::Descendants => {
let records = collect_xml_descendants(scope, inference_options);
if records.is_empty() {
let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
bail!(
"XML scope '{scope_name}' does not contain any leaf text elements (--xml-mode descendants)"
);
}
records
}
XmlMode::Attributes => {
let records = collect_xml_attributes_records(scope, inference_options);
if records.is_empty() {
let scope_name = requested_sheet.unwrap_or(scope.tag_name().name());
bail!(
"XML scope '{scope_name}' does not contain any elements with attributes (--xml-mode attributes)"
);
}
records
}
};
let mut columns = Vec::new();
for row in &records {
for (column, _) in row {
if !columns.iter().any(|existing| existing == column) {
columns.push(column.clone());
}
}
}
let rows = records
.into_iter()
.map(|row| {
columns
.iter()
.map(|column| {
row.iter()
.find(|(key, _)| key == column)
.map(|(_, value)| value.clone())
.unwrap_or(QueryValue::Null)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let original_name = requested_sheet
.map(str::to_owned)
.unwrap_or_else(|| scope.tag_name().name().to_owned());
Ok(SheetData {
original_name,
columns,
rows,
})
}
fn collect_xml_records(
scope: Node<'_, '_>,
inference_options: &TypeInferenceOptions,
) -> Vec<Vec<(String, QueryValue)>> {
let direct_row_nodes = scope
.children()
.filter(|node| node.is_element() && node.tag_name().name().eq_ignore_ascii_case("row"))
.collect::<Vec<_>>();
if !direct_row_nodes.is_empty() {
return direct_row_nodes
.into_iter()
.map(|row| xml_row_from_children(row, inference_options))
.filter(|row| !row.is_empty())
.collect();
}
scope
.descendants()
.filter(|node| node.is_element() && *node != scope)
.filter(|node| is_xml_record_candidate(*node))
.map(|row| xml_row_from_children(row, inference_options))
.filter(|row| !row.is_empty())
.collect()
}
fn is_xml_record_candidate(node: Node<'_, '_>) -> bool {
let children = node
.children()
.filter(|child| child.is_element())
.collect::<Vec<_>>();
!children.is_empty()
&& children
.iter()
.all(|child| !child.children().any(|inner| inner.is_element()))
}
fn xml_row_from_children(
node: Node<'_, '_>,
inference_options: &TypeInferenceOptions,
) -> Vec<(String, QueryValue)> {
node.children()
.filter(|child| child.is_element())
.map(|child| {
let column = child.tag_name().name().to_owned();
let value = xml_text_to_query_value(&xml_text_content(child), inference_options);
(column, value)
})
.collect()
}
fn xml_text_content(node: Node<'_, '_>) -> String {
node.text().unwrap_or_default().trim().to_owned()
}
fn xml_text_to_query_value(raw: &str, inference_options: &TypeInferenceOptions) -> QueryValue {
parse_scalar_value(raw, inference_options)
}
fn collect_xml_descendants(
scope: Node<'_, '_>,
inference_options: &TypeInferenceOptions,
) -> Vec<Vec<(String, QueryValue)>> {
scope
.descendants()
.filter(|node| node.is_element() && *node != scope)
.filter(|node| !node.children().any(|child| child.is_element()))
.map(|node| {
let tag = node.tag_name().name().to_owned();
let value = xml_text_to_query_value(&xml_text_content(node), inference_options);
vec![
("tag".to_owned(), QueryValue::Text(tag)),
("value".to_owned(), value),
]
})
.collect()
}
fn collect_xml_attributes_records(
scope: Node<'_, '_>,
inference_options: &TypeInferenceOptions,
) -> Vec<Vec<(String, QueryValue)>> {
scope
.descendants()
.filter(|node| node.is_element() && *node != scope)
.filter(|node| node.attributes().count() > 0)
.map(|node| {
let mut row: Vec<(String, QueryValue)> = node
.attributes()
.map(|attr| {
let column = attr.name().to_owned();
let value = xml_text_to_query_value(attr.value(), inference_options);
(column, value)
})
.collect();
let text = xml_text_content(node);
if !text.is_empty() {
row.push((
"value".to_owned(),
xml_text_to_query_value(&text, inference_options),
));
}
row
})
.filter(|row| !row.is_empty())
.collect()
}