animsmith-core 0.1.0

Engine-agnostic data model, sampling, measurements, and checks for the animsmith animation-clip linter
Documentation
use animsmith_core::all_checks;
use std::collections::BTreeSet;

const README: &str = include_str!("../../../README.md");
const GAME_READY_CLIPS: &str = include_str!("../../../docs/game-ready-clips.md");

const NON_CHECK_ID_LIKE_TOKENS: &[&str] = &[
    "animsmith",
    "animsmith-core",
    "animsmith-fbx",
    "animsmith-gltf",
    "animsmith-report",
    "fix",
    "humanoid",
    "measure",
    "mixamo",
    "transform",
    "ue-mannequin",
];

#[test]
fn docs_check_ids_match_the_registered_catalog() {
    let catalog = registered_check_ids();

    assert_catalog_ids(
        "README.md check tables",
        &readme_check_table_ids(),
        &catalog,
    );
    assert_catalog_ids(
        "docs/game-ready-clips.md symptom table",
        &guide_symptom_table_ids(),
        &catalog,
    );

    for (path, markdown) in [
        ("README.md", README),
        ("docs/game-ready-clips.md", GAME_READY_CLIPS),
    ] {
        let tokens = inline_code_tokens(markdown);
        let documented: BTreeSet<_> = tokens
            .iter()
            .copied()
            .filter(|token| catalog.contains(token))
            .collect();
        assert_eq!(
            documented, catalog,
            "{path} inline-code scan must see the registered check ids"
        );

        let unknown_check_ids: Vec<_> = tokens
            .iter()
            .copied()
            .filter(|token| looks_like_check_id(token))
            .filter(|token| !catalog.contains(token))
            .filter(|token| !NON_CHECK_ID_LIKE_TOKENS.contains(token))
            .collect();
        assert!(
            unknown_check_ids.is_empty(),
            "{path} names check-like ids that are not registered: {unknown_check_ids:?}"
        );
    }
}

fn assert_catalog_ids(surface: &str, documented: &BTreeSet<&str>, catalog: &BTreeSet<&str>) {
    let missing: Vec<_> = catalog
        .iter()
        .copied()
        .filter(|id| !documented.contains(id))
        .collect();
    assert!(
        missing.is_empty(),
        "{surface} does not document registered checks: {missing:?}"
    );

    let unknown: Vec<_> = documented
        .iter()
        .copied()
        .filter(|id| !catalog.contains(id))
        .collect();
    assert!(
        unknown.is_empty(),
        "{surface} documents checks that are not registered: {unknown:?}"
    );
}

fn registered_check_ids() -> BTreeSet<&'static str> {
    let checks = all_checks();
    let ids: Vec<_> = checks.iter().map(|check| check.id()).collect();
    let unique: BTreeSet<_> = ids.iter().copied().collect();
    assert_eq!(ids.len(), unique.len(), "duplicate registered check id");
    unique
}

fn readme_check_table_ids() -> BTreeSet<&'static str> {
    let tables = [
        markdown_table_after(README, "Mechanical checks"),
        markdown_table_after(README, "Contract-aware checks"),
    ];
    tables
        .into_iter()
        .flat_map(|table| table.into_iter().skip(2))
        .filter_map(|row| table_cell(row, 0))
        .flat_map(inline_code_tokens)
        .collect()
}

fn guide_symptom_table_ids() -> BTreeSet<&'static str> {
    markdown_table_after(GAME_READY_CLIPS, "From symptom to command")
        .into_iter()
        .skip(2)
        .filter_map(|row| table_cell(row, 1))
        .flat_map(inline_code_tokens)
        .collect()
}

fn markdown_table_after(markdown: &'static str, marker: &str) -> Vec<&'static str> {
    let mut lines = markdown.lines().skip_while(|line| !line.contains(marker));
    let Some(_) = lines.next() else {
        panic!("missing marker: {marker}");
    };
    lines
        .skip_while(|line| line.trim().is_empty())
        .take_while(|line| line.trim_start().starts_with('|'))
        .collect()
}

fn table_cell(row: &str, index: usize) -> Option<&str> {
    row.trim()
        .trim_matches('|')
        .split('|')
        .map(str::trim)
        .nth(index)
}

fn inline_code_tokens(markdown: &str) -> Vec<&str> {
    let mut tokens = Vec::new();
    let mut in_fence = false;

    for line in markdown.lines() {
        if line.trim_start().starts_with("```") {
            in_fence = !in_fence;
            continue;
        }
        if in_fence {
            continue;
        }
        let mut rest = line;
        while let Some(start) = rest.find('`') {
            let after_start = &rest[start + 1..];
            let Some(end) = after_start.find('`') else {
                break;
            };
            tokens.push(&after_start[..end]);
            rest = &after_start[end + 1..];
        }
    }

    tokens
}

fn looks_like_check_id(token: &str) -> bool {
    !token.starts_with('-')
        && !token.ends_with('-')
        && token.chars().all(|ch| ch.is_ascii_lowercase() || ch == '-')
}