docspec-docx-reader 1.21.9

DOCX to DocSpec event stream reader
Documentation
//! Build configuration for `docspec-docx-reader`.

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())
}