use crate::exc::{messages, PptxError, Result};
use std::collections::HashSet;
pub type RequiredPart = (&'static str, &'static str);
pub const REQUIRED_PARTS_MINIMAL: &[&str] = &[
"[Content_Types].xml",
"_rels/.rels",
"ppt/presentation.xml",
"docProps/core.xml",
];
pub const REQUIRED_PARTS_REPAIR: &[RequiredPart] = &[
("[Content_Types].xml", "Content types definition"),
("_rels/.rels", "Package relationships"),
("ppt/presentation.xml", "Presentation document"),
(
"ppt/_rels/presentation.xml.rels",
"Presentation relationships",
),
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ValidationIssue {
MissingPart {
path: String,
description: Option<String>,
},
EmptyXml {
path: String,
},
InvalidXml {
path: String,
error: String,
},
}
impl ValidationIssue {
pub fn message(&self) -> String {
match self {
ValidationIssue::MissingPart { path, description } => match description {
Some(desc) => format!("Missing required part '{path}' ({desc})"),
None => messages::missing_part(path),
},
ValidationIssue::EmptyXml { path } => messages::empty_xml(path),
ValidationIssue::InvalidXml { path, error } => messages::invalid_xml(path, error),
}
}
}
pub fn clamp_ratio(value: f64) -> f64 {
value.clamp(0.0, 1.0)
}
pub fn clamp_unit_interval(value: f64) -> f64 {
value.clamp(0.0, 1.0)
}
pub fn validate_non_empty_str(value: &str, field: &str) -> Result<()> {
if value.trim().is_empty() {
return Err(PptxError::InvalidValue(messages::must_not_be_empty(field)));
}
Ok(())
}
pub fn validate_non_empty<T>(items: &[T], field: &str) -> Result<()> {
if items.is_empty() {
return Err(PptxError::InvalidState(messages::must_not_be_empty(field)));
}
Ok(())
}
pub fn validate_index(index: usize, count: usize, field: &str) -> Result<()> {
if index >= count {
return Err(PptxError::NotFound(messages::index_out_of_range(
field, index, count,
)));
}
Ok(())
}
pub fn validate_positive(value: u32, field: &str) -> Result<()> {
if value == 0 {
return Err(PptxError::InvalidValue(messages::must_be_positive(field)));
}
Ok(())
}
pub fn validate_well_formed_xml(xml: &str) -> Result<()> {
let trimmed = xml.trim();
if trimmed.is_empty() {
return Err(PptxError::InvalidXml(messages::empty_xml_content()));
}
let mut in_tag = false;
let mut in_string = false;
let mut string_char = '"';
for ch in trimmed.chars() {
match ch {
'"' | '\'' if in_tag && !in_string => {
in_string = true;
string_char = ch;
}
c if in_string && c == string_char => {
in_string = false;
}
'<' if !in_string => {
in_tag = true;
}
'>' if !in_string => {
in_tag = false;
}
_ => {}
}
}
Ok(())
}
pub fn check_required_parts(
found: &HashSet<String>,
required: &[&str],
) -> Vec<ValidationIssue> {
required
.iter()
.filter(|path| !found.contains(**path))
.map(|path| ValidationIssue::MissingPart {
path: (*path).to_string(),
description: None,
})
.collect()
}
pub fn check_required_parts_with_descriptions(
has_part: impl Fn(&str) -> bool,
required: &[RequiredPart],
) -> Vec<ValidationIssue> {
required
.iter()
.filter(|(path, _)| !has_part(path))
.map(|(path, description)| ValidationIssue::MissingPart {
path: (*path).to_string(),
description: Some((*description).to_string()),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clamp_ratio() {
assert_eq!(clamp_ratio(0.5), 0.5);
assert_eq!(clamp_ratio(-1.0), 0.0);
assert_eq!(clamp_ratio(2.0), 1.0);
}
#[test]
fn test_validate_non_empty_str() {
assert!(validate_non_empty_str("hello", "title").is_ok());
assert!(validate_non_empty_str(" ", "title").is_err());
}
#[test]
fn test_validate_index() {
assert!(validate_index(0, 3, "slide").is_ok());
assert!(validate_index(3, 3, "slide").is_err());
}
#[test]
fn test_validate_well_formed_xml() {
assert!(validate_well_formed_xml("<root/>").is_ok());
assert!(validate_well_formed_xml("").is_err());
assert!(validate_well_formed_xml(" ").is_err());
}
#[test]
fn test_check_required_parts() {
let found: HashSet<_> = ["a.xml", "b.xml"].into_iter().map(str::to_string).collect();
let issues = check_required_parts(&found, &["a.xml", "c.xml"]);
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].message(), "Missing required part: c.xml");
}
}