#![allow(dead_code)]
use crate::types::ProcessingWarning;
use std::borrow::Cow;
const MAX_NAMED_ENTRIES: usize = 10;
pub(crate) fn warning(source: &'static str, message: impl Into<String>) -> ProcessingWarning {
ProcessingWarning {
source: Cow::Borrowed(source),
message: Cow::Owned(message.into()),
}
}
pub(crate) fn push_warning(accumulated: &mut Vec<ProcessingWarning>, source: &'static str, message: impl Into<String>) {
push_warning_deduped(accumulated, warning(source, message));
}
pub(crate) fn push_truncated_parse_warning(
accumulated: &mut Vec<ProcessingWarning>,
source: &'static str,
stage: &str,
cause: &dyn std::fmt::Display,
) {
let message = format!(
"Parsing of {stage} stopped early at a malformed XML event; \
the remaining content was not extracted (cause: {cause})"
);
push_warning(accumulated, source, message);
}
pub(crate) fn push_unclosed_elements_warning(
accumulated: &mut Vec<ProcessingWarning>,
source: &'static str,
unclosed: &[String],
) {
if unclosed.is_empty() {
return;
}
let message = format!(
"Input ended with {} unclosed element{} ({}); the document is truncated \
and trailing content was not extracted",
unclosed.len(),
if unclosed.len() == 1 { "" } else { "s" },
format_entry_list(unclosed)
);
push_warning(accumulated, source, message);
}
pub(crate) fn push_lossy_decode_warning(accumulated: &mut Vec<ProcessingWarning>, source: &'static str, subject: &str) {
let message = format!(
"The {subject} is not valid UTF-8; the undecodable bytes were replaced with the Unicode \
replacement character, so those characters are missing from the extracted text. \
Re-encode the input as UTF-8 to recover them"
);
push_warning(accumulated, source, message);
}
pub(crate) fn format_entry_list(names: &[String]) -> String {
if names.len() <= MAX_NAMED_ENTRIES {
return names.join(", ");
}
format!(
"{}, and {} more",
names[..MAX_NAMED_ENTRIES].join(", "),
names.len() - MAX_NAMED_ENTRIES
)
}
pub(crate) fn push_warning_deduped(accumulated: &mut Vec<ProcessingWarning>, warning: ProcessingWarning) {
if !accumulated
.iter()
.any(|existing| existing.source == warning.source && existing.message == warning.message)
{
accumulated.push(warning);
}
}
#[cfg(all(feature = "pdf", any(feature = "ocr", feature = "ocr-pipeline")))]
pub(crate) fn dedup_extend_warnings(accumulated: &mut Vec<ProcessingWarning>, new: Vec<ProcessingWarning>) {
for warning in new {
push_warning_deduped(accumulated, warning);
}
}
#[cfg(test)]
mod convention_tests {
use super::*;
#[test]
fn should_collapse_repeated_push_warning_for_same_source_and_message() {
let mut accumulated = Vec::new();
push_warning(&mut accumulated, "xml", "lost the tail");
push_warning(&mut accumulated, "xml", "lost the tail");
assert_eq!(accumulated.len(), 1);
assert_eq!(accumulated[0].source, "xml");
assert_eq!(accumulated[0].message, "lost the tail");
}
#[test]
fn should_name_the_cause_and_stage_in_truncation_warning() {
let mut accumulated = Vec::new();
push_truncated_parse_warning(&mut accumulated, "fictionbook", "the document body", &"boom");
assert_eq!(accumulated[0].source, "fictionbook");
assert_eq!(
accumulated[0].message,
"Parsing of the document body stopped early at a malformed XML event; \
the remaining content was not extracted (cause: boom)"
);
}
#[test]
fn should_elide_entry_list_past_the_named_maximum() {
let short: Vec<String> = vec!["a.txt".into(), "b.txt".into()];
assert_eq!(format_entry_list(&short), "a.txt, b.txt");
let long: Vec<String> = (0..13).map(|i| format!("f{i}")).collect();
assert_eq!(
format_entry_list(&long),
"f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, and 3 more"
);
}
}
#[cfg(all(test, feature = "pdf", any(feature = "ocr", feature = "ocr-pipeline")))]
mod tests {
use super::*;
use std::borrow::Cow;
fn warning(source: &'static str, message: &str) -> ProcessingWarning {
ProcessingWarning {
source: Cow::Borrowed(source),
message: Cow::Owned(message.to_string()),
}
}
#[test]
fn dedup_extend_drops_identical_keeps_distinct() {
let mut accumulated = vec![warning("paddle-ocr", "a")];
dedup_extend_warnings(
&mut accumulated,
vec![warning("paddle-ocr", "a"), warning("paddle-ocr", "b")],
);
dedup_extend_warnings(
&mut accumulated,
vec![warning("paddle-ocr", "a"), warning("paddle-ocr", "b")],
);
let messages: Vec<&str> = accumulated.iter().map(|w| w.message.as_ref()).collect();
assert_eq!(messages, vec!["a", "b"]);
}
#[test]
fn same_message_different_source_is_kept() {
let mut accumulated = vec![warning("layout", "failed")];
push_warning_deduped(&mut accumulated, warning("ocr", "failed"));
push_warning_deduped(&mut accumulated, warning("layout", "failed"));
assert_eq!(accumulated.len(), 2, "distinct sources must not collapse");
}
}