use crate::parse::Node;
const INDEXED_ELEMENTS: &[&str] = &[
"Ad",
"Creative",
"MediaFile",
"Mezzanine",
"InteractiveCreativeFile",
"Icon",
"Tracking",
"Verification",
"Companion",
"NonLinear",
"Extension",
"CreativeExtension",
];
pub(super) struct Location<'a> {
base: &'a str,
stack: &'a [(&'a str, Option<usize>)],
}
impl Location<'_> {
pub fn path(&self) -> String {
let mut out = String::with_capacity(self.base.len() + self.stack.len() * 16);
out.push_str(self.base);
for (name, index) in self.stack {
out.push('/');
out.push_str(name);
if let Some(i) = index {
out.push('[');
out.push_str(itoa(*i).as_str());
out.push(']');
}
}
out
}
pub fn inside(&self, name: &str) -> bool {
let ancestors = &self.stack[..self.stack.len().saturating_sub(1)];
ancestors.iter().any(|(n, _)| *n == name)
}
}
fn itoa(mut n: usize) -> String {
if n == 0 {
return "0".to_owned();
}
let mut buf = [0u8; 20];
let mut i = buf.len();
while n > 0 {
i -= 1;
buf[i] = b'0' + (n % 10) as u8;
n /= 10;
}
String::from_utf8_lossy(&buf[i..]).into_owned()
}
pub(super) fn visit_vast_elements<F>(node: &Node, base: &str, f: &mut F)
where
F: FnMut(&Node, &Location),
{
let mut stack: Vec<(&str, Option<usize>)> = Vec::new();
visit_inner(node, base, &mut stack, f);
}
fn visit_inner<'a, F>(
node: &'a Node,
base: &str,
stack: &mut Vec<(&'a str, Option<usize>)>,
f: &mut F,
) where
F: FnMut(&Node, &Location),
{
let mut seen: Vec<(&str, usize)> = Vec::new();
for child in &node.children {
let name = child.name.as_str();
let segment = if INDEXED_ELEMENTS.contains(&name) {
let index = match seen.iter_mut().find(|(n, _)| *n == name) {
Some((_, count)) => {
let i = *count;
*count += 1;
i
}
None => {
seen.push((name, 1));
0
}
};
(name, Some(index))
} else {
(name, None)
};
stack.push(segment);
f(child, &Location { base, stack });
let is_vendor_extension = matches!(name, "Extension" | "CreativeExtension")
&& !child.is_standardised_iab_extension();
if !is_vendor_extension {
visit_inner(child, base, stack, f);
}
stack.pop();
}
}
#[cfg(test)]
mod tests {
use super::visit_vast_elements;
use crate::parse::parse;
fn paths(xml: &str) -> Vec<String> {
let doc = parse(xml);
let mut out = Vec::new();
visit_vast_elements(&doc.root, "/VAST", &mut |_, loc| out.push(loc.path()));
out
}
#[test]
fn location_reports_the_ancestor_chain() {
let doc = parse(
r#"<VAST version="4.2"><Ad id="a"><InLine><Creatives><Creative id="c"><NonLinearAds>
<NonLinear id="nl"><MediaFiles><MediaFile/></MediaFiles></NonLinear>
</NonLinearAds></Creative></Creatives></InLine></Ad></VAST>"#,
);
let mut found = false;
visit_vast_elements(&doc.root, "/VAST", &mut |node, loc| {
if node.name == "MediaFile" {
found = true;
assert!(loc.inside("MediaFiles"));
assert!(loc.inside("NonLinear"));
assert!(!loc.inside("Linear"));
}
});
assert!(found, "MediaFile was never visited");
}
#[test]
fn repeated_siblings_are_indexed_and_singletons_are_not() {
let got = paths(
r#"<VAST version="2.0"><Ad id="a"><InLine><AdTitle>T</AdTitle>
<Creatives><Creative id="c1"/><Creative id="c2"/></Creatives>
</InLine></Ad></VAST>"#,
);
assert!(got.contains(&"/VAST/Ad[0]/InLine/AdTitle".to_string()));
assert!(got.contains(&"/VAST/Ad[0]/InLine/Creatives/Creative[0]".to_string()));
assert!(got.contains(&"/VAST/Ad[0]/InLine/Creatives/Creative[1]".to_string()));
}
#[test]
fn vendor_extension_payload_is_not_entered() {
let got = paths(
r#"<VAST version="2.0"><Ad id="a"><InLine><Extensions>
<Extension type="acme-private"><MediaFiles><MediaFile/></MediaFiles></Extension>
</Extensions></InLine></Ad></VAST>"#,
);
assert!(got.iter().any(|p| p.ends_with("/Extension[0]")));
assert!(
!got.iter().any(|p| p.contains("MediaFile")),
"walker must stop at a vendor extension: {:?}",
got
);
}
#[test]
fn standardised_extension_payload_is_entered() {
let got = paths(
r#"<VAST version="2.0"><Ad id="a"><InLine><Extensions>
<Extension type="ctv_ad_portfolio"><MediaFiles><MediaFile/></MediaFiles></Extension>
</Extensions></InLine></Ad></VAST>"#,
);
assert!(
got.iter()
.any(|p| p.ends_with("/Extension[0]/MediaFiles/MediaFile[0]")),
"walker must enter a standardised IAB container: {:?}",
got
);
}
}