use super::emit;
use crate::parse::{Node, VastDocument};
use crate::{DetectedVersion, Issue, Severity, ValidationContext};
pub fn check(
doc: &VastDocument,
_version: &DetectedVersion,
ctx: &ValidationContext,
issues: &mut Vec<Issue>,
) {
let Some(vast) = doc.vast_root() else { return };
for (ad_idx, ad) in vast.children_named("Ad").enumerate() {
let ad_path = format!("/VAST/Ad[{}]", ad_idx);
if let Some(inline) = ad.child("InLine") {
check_inline(inline, &format!("{}/InLine", ad_path), ctx, issues);
}
if let Some(wrapper) = ad.child("Wrapper") {
if let Some(creatives) = wrapper.child("Creatives") {
for (ci, creative) in creatives.children_named("Creative").enumerate() {
let cp = format!("{}/Wrapper/Creatives/Creative[{}]", ad_path, ci);
check_nonlinear_ads(creative, &cp, ctx, issues);
}
}
}
}
}
fn check_inline(inline: &Node, inline_path: &str, ctx: &ValidationContext, issues: &mut Vec<Issue>) {
let Some(creatives) = inline.child("Creatives") else { return };
for (ci, creative) in creatives.children_named("Creative").enumerate() {
let cp = format!("{}/Creatives/Creative[{}]", inline_path, ci);
if let Some(linear) = creative.child("Linear") {
let lp = format!("{}/Linear", cp);
if let Some(mf) = linear.child("MediaFiles") {
let mf_path = format!("{}/MediaFiles", lp);
check_interactive_creative_files(mf, &mf_path, &lp, linear, ctx, issues);
}
}
check_nonlinear_ads(creative, &cp, ctx, issues);
}
}
fn check_nonlinear_ads(creative: &Node, cp: &str, ctx: &ValidationContext, issues: &mut Vec<Issue>) {
let Some(nl_ads) = creative.child("NonLinearAds") else { return };
for (ni, nl) in nl_ads.children_named("NonLinear").enumerate() {
let nl_path = format!("{}/NonLinearAds/NonLinear[{}]", cp, ni);
let nl_api = nl.attr("apiFramework").unwrap_or("");
if nl_api == "SIMID" {
let has_iframe = nl.children_named("IFrameResource").next().is_some();
if !has_iframe {
emit(
ctx,
issues,
"SIMID-1.1-nonlinear-simid-no-iframe",
Severity::Error,
"<NonLinear apiFramework=\"SIMID\"> must contain an <IFrameResource> with the SIMID creative URL",
Some(nl_path.clone()),
"IAB SIMID 1.1 §3.5.1",
Some(nl),
);
}
for (ri, iframe) in nl.children_named("IFrameResource").enumerate() {
let iframe_path = format!("{}/IFrameResource[{}]", nl_path, ri);
check_iframe_resource(iframe, &iframe_path, ctx, issues);
}
continue; }
for (ri, iframe) in nl.children_named("IFrameResource").enumerate() {
let iframe_path = format!("{}/IFrameResource[{}]", nl_path, ri);
if iframe.attr("apiFramework").map(|v| v == "SIMID").unwrap_or(false) {
check_iframe_resource(iframe, &iframe_path, ctx, issues);
}
}
}
}
fn check_interactive_creative_files(
mf_node: &Node,
mf_path: &str,
linear_path: &str,
linear_node: &Node,
ctx: &ValidationContext,
issues: &mut Vec<Issue>,
) {
let mut has_simid = false;
let mut has_video_mediafile = false;
for mf in mf_node.children_named("MediaFile") {
let t = mf.attr("type").unwrap_or("");
if t.starts_with("video/") || t.starts_with("audio/") || t == "application/x-mpegURL" {
has_video_mediafile = true;
}
}
for (icf_i, icf) in mf_node.children_named("InteractiveCreativeFile").enumerate() {
let icf_path = format!("{}/InteractiveCreativeFile[{}]", mf_path, icf_i);
let api = icf.attr("apiFramework").unwrap_or("");
if api != "SIMID" {
continue;
}
has_simid = true;
let declared_type = icf.attr("type").unwrap_or("");
if declared_type != "text/html" {
emit(
ctx,
issues,
"SIMID-1.0-simid-type-required",
Severity::Error,
"<InteractiveCreativeFile apiFramework=\"SIMID\"> must have type=\"text/html\" per SIMID §5",
Some(icf_path.clone()),
"IAB SIMID 1.0 §5",
Some(icf),
);
}
let url = icf.text.trim();
if url.is_empty() {
emit(
ctx,
issues,
"SIMID-1.0-simid-url-empty",
Severity::Error,
"<InteractiveCreativeFile apiFramework=\"SIMID\"> must contain a URL — text content is empty",
Some(icf_path.clone()),
"IAB SIMID 1.0 §3.1",
Some(icf),
);
} else if url.starts_with("http://") {
emit(
ctx,
issues,
"SIMID-1.0-simid-url-https",
Severity::Error,
"<InteractiveCreativeFile apiFramework=\"SIMID\"> URL must use HTTPS — HTTP will be blocked in secure contexts",
Some(icf_path.clone()),
"IAB SIMID 1.0 §3.1",
Some(icf),
);
}
if let Some(vd) = icf.attr("variableDuration") {
if vd != "true" {
emit(
ctx,
issues,
"SIMID-1.0-simid-variable-duration-value",
Severity::Warning,
"<InteractiveCreativeFile apiFramework=\"SIMID\"> variableDuration must be \"true\" when present per SIMID §5",
Some(icf_path.clone()),
"IAB SIMID 1.0 §5",
Some(icf),
);
}
}
}
if has_simid && !has_video_mediafile {
emit(
ctx,
issues,
"SIMID-1.0-simid-mediafile-required",
Severity::Error,
"Linear ad with <InteractiveCreativeFile apiFramework=\"SIMID\"> must also include a video/audio <MediaFile> — SIMID requires a media asset",
Some(linear_path.to_owned()),
"IAB SIMID 1.0 §3.4",
Some(linear_node),
);
}
}
fn check_iframe_resource(
iframe: &Node,
iframe_path: &str,
ctx: &ValidationContext,
issues: &mut Vec<Issue>,
) {
let declared_type = iframe.attr("type").unwrap_or("");
if declared_type != "text/html" {
emit(
ctx,
issues,
"SIMID-1.1-iframe-simid-type-required",
Severity::Warning,
"<IFrameResource> in SIMID <NonLinear> should have type=\"text/html\" per SIMID §3.5.1",
Some(iframe_path.to_owned()),
"IAB SIMID 1.1 §3.5.1",
Some(iframe),
);
}
let url = iframe.text.trim();
if url.is_empty() {
emit(
ctx,
issues,
"SIMID-1.1-iframe-simid-url-empty",
Severity::Error,
"<IFrameResource> in SIMID <NonLinear> must contain a URL — text content is empty",
Some(iframe_path.to_owned()),
"IAB SIMID 1.1 §3.5.1",
Some(iframe),
);
} else if url.starts_with("http://") {
emit(
ctx,
issues,
"SIMID-1.1-iframe-simid-url-https",
Severity::Error,
"<IFrameResource> in SIMID <NonLinear> URL must use HTTPS — HTTP will be blocked in secure contexts",
Some(iframe_path.to_owned()),
"IAB SIMID 1.1 §3.5.1",
Some(iframe),
);
}
}