use core::fmt::Write as _;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
fn main() -> Result<(), Box<dyn core::error::Error>> {
println!("cargo::rustc-check-cfg=cfg(coverage)");
generate_corpus_tests("pandoc", "pandoc_corpus_tests.rs")?;
generate_corpus_tests("docspec", "docspec_corpus_tests.rs")?;
generate_corpus_tests("apache-tika", "apache_tika_corpus_tests.rs")?;
Ok(())
}
fn generate_corpus_tests(
corpus: &str,
output_file: &str,
) -> Result<(), Box<dyn core::error::Error>> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let fixture_dir = manifest_dir.join(format!("../../tests/fixtures/docx/{corpus}"));
println!("cargo::rerun-if-changed={}", fixture_dir.display());
let corpus_literal = rust_string_literal(corpus);
let mut generated = String::from("// @generated by crates/docspec-docx-reader/build.rs\n\n");
if fixture_dir.try_exists()? {
let mut fixtures = fs::read_dir(&fixture_dir)?
.filter_map(|entry_result| fixture_file_name(entry_result).transpose())
.collect::<Result<Vec<_>, _>>()?;
fixtures.sort();
for fixture in fixtures {
let test_name = test_name_for_fixture(&fixture)?;
let fixture_literal = rust_string_literal(&fixture);
writeln!(
&mut generated,
"#[test]\nfn {test_name}() {{\n super::assert_fixture_snapshot({corpus_literal}, {fixture_literal});\n}}\n"
)?;
}
}
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
fs::write(out_dir.join(output_file), generated)?;
Ok(())
}
fn rust_string_literal(value: &str) -> String {
let mut literal = String::from("\"");
for character in value.chars() {
literal.extend(character.escape_default());
}
literal.push('"');
literal
}
fn fixture_file_name(entry_result: io::Result<fs::DirEntry>) -> io::Result<Option<String>> {
let directory_entry = entry_result?;
let path = directory_entry.path();
if path.extension() != Some(OsStr::new("docx")) {
return Ok(None);
}
path.file_name()
.and_then(OsStr::to_str)
.map(|file_name| Some(file_name.to_owned()))
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "fixture filename is not UTF-8"))
}
fn test_name_for_fixture(file_name: &str) -> Result<String, Box<dyn core::error::Error>> {
let stem = Path::new(file_name)
.file_stem()
.and_then(OsStr::to_str)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "fixture stem is not UTF-8"))?;
let test_name = sanitize_test_name(stem);
if is_valid_rust_identifier(&test_name) {
Ok(test_name)
} else {
Err(format!("Fixture {file_name:?} does not map to a valid Rust test name").into())
}
}
fn sanitize_test_name(stem: &str) -> String {
let mut test_name = String::new();
let mut previous_was_separator = true;
let mut previous_was_lower_or_digit = false;
for character in stem.chars() {
if character.is_ascii_alphanumeric() {
if character.is_ascii_uppercase() && previous_was_lower_or_digit {
test_name.push('_');
}
test_name.push(character.to_ascii_lowercase());
previous_was_separator = false;
previous_was_lower_or_digit =
character.is_ascii_lowercase() || character.is_ascii_digit();
continue;
}
if !previous_was_separator {
test_name.push('_');
previous_was_separator = true;
previous_was_lower_or_digit = false;
}
}
while test_name.ends_with('_') {
test_name.pop();
}
if test_name.is_empty() {
test_name.push('_');
} else {
let starts_with_invalid_character = test_name
.chars()
.next()
.is_some_and(|first| !first.is_ascii_alphabetic() && first != '_');
if starts_with_invalid_character {
test_name.insert(0, '_');
}
}
test_name
}
fn is_valid_rust_identifier(identifier: &str) -> bool {
let mut chars = identifier.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}