use crate::errors::XmlError;
use alloc::collections::BTreeSet;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
pub const MAX_INHERITANCE_DEPTH: usize = 32;
pub fn resolve_chain<F>(name: &str, mut lookup: F) -> Result<Vec<String>, XmlError>
where
F: FnMut(&str) -> Result<Option<String>, XmlError>,
{
let mut visited: BTreeSet<String> = BTreeSet::new();
let mut chain: Vec<String> = Vec::new();
let mut current = name.to_string();
for _ in 0..MAX_INHERITANCE_DEPTH {
if !visited.insert(current.clone()) {
chain.push(current.clone());
let pretty = chain.join(" -> ");
return Err(XmlError::CircularInheritance(pretty));
}
chain.push(current.clone());
match lookup(¤t)? {
None => {
chain.reverse();
return Ok(chain);
}
Some(base) => {
current = base;
}
}
}
Err(XmlError::LimitExceeded(format!(
"base_name chain depth > {MAX_INHERITANCE_DEPTH}"
)))
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use alloc::collections::BTreeMap;
use alloc::vec;
fn make_lookup(
items: BTreeMap<&'static str, Option<&'static str>>,
) -> impl FnMut(&str) -> Result<Option<String>, XmlError> {
move |name: &str| {
items
.get(name)
.copied()
.ok_or_else(|| XmlError::MissingRequiredElement(name.to_string()))
.map(|opt| opt.map(|s| s.to_string()))
}
}
#[test]
fn no_inheritance() {
let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
items.insert("A", None);
let chain = resolve_chain("A", make_lookup(items)).expect("ok");
assert_eq!(chain, vec!["A".to_string()]);
}
#[test]
fn three_level_chain() {
let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
items.insert("A", Some("B"));
items.insert("B", Some("C"));
items.insert("C", None);
let chain = resolve_chain("A", make_lookup(items)).expect("ok");
assert_eq!(
chain,
vec!["C".to_string(), "B".to_string(), "A".to_string()]
);
}
#[test]
fn two_node_cycle() {
let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
items.insert("A", Some("B"));
items.insert("B", Some("A"));
let err = resolve_chain("A", make_lookup(items)).expect_err("cycle");
match err {
XmlError::CircularInheritance(msg) => {
assert!(msg.contains("A -> B -> A") || msg.contains("A"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn self_cycle() {
let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
items.insert("A", Some("A"));
let err = resolve_chain("A", make_lookup(items)).expect_err("self-cycle");
assert!(matches!(err, XmlError::CircularInheritance(_)));
}
#[test]
fn missing_base_propagates() {
let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
items.insert("A", Some("DOES_NOT_EXIST"));
let err = resolve_chain("A", make_lookup(items)).expect_err("missing");
assert!(matches!(err, XmlError::MissingRequiredElement(_)));
}
#[test]
fn depth_cap_enforced() {
let lookup = |name: &str| -> Result<Option<String>, XmlError> {
Ok(Some(format!("{name}x")))
};
let err = resolve_chain("A", lookup).expect_err("depth");
assert!(matches!(err, XmlError::LimitExceeded(_)));
}
}