aion-core 0.31.0

Pure domain model and shared vocabulary for Aion durable workflows.
Documentation
//! `docs/reference/events.md` is a rendering of the doc comments on
//! [`aion_core::Event`], and this test holds the two together.
//!
//! ESTABLISHED: every variant `event.rs` declares has a `### Name` section in
//! the reference, in declaration order; every section names a declared
//! variant; and every field a variant carries appears in that section's
//! field table. A variant or field added, removed, or renamed without
//! re-rendering fails here.
//!
//! NOT ESTABLISHED: that the prose under a heading is current. The renderer
//! copies it from the same doc comment, so re-rendering is the fix either
//! way; the check is on the STRUCTURE the renderer cannot get wrong.

use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};

fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(Path::parent)
        .map_or_else(|| PathBuf::from("../.."), Path::to_path_buf)
}

/// `(variant, fields)` for every variant of `pub enum Event`, read from the
/// source the way the renderer reads it.
fn declared_variants(source: &str) -> Result<Vec<(String, BTreeSet<String>)>, String> {
    let start = source
        .find("pub enum Event {")
        .ok_or("event.rs no longer declares `pub enum Event {`")?;
    let body = &source[start..];
    let end = body
        .find("\n}\n")
        .ok_or("the Event enum body has no closing brace")?;
    let mut variants = Vec::new();
    let mut current: Option<(String, BTreeSet<String>)> = None;
    for line in body[..end].lines().skip(1) {
        if let Some(rest) = line.strip_prefix("    ")
            && !rest.starts_with(' ')
            && let Some(name) = rest.strip_suffix(" {")
            && name.chars().next().is_some_and(char::is_uppercase)
        {
            current = Some((name.to_owned(), BTreeSet::new()));
            continue;
        }
        if line == "    }," || line == "    }" {
            if let Some(done) = current.take() {
                variants.push(done);
            }
            continue;
        }
        if let Some((_, fields)) = current.as_mut()
            && let Some(rest) = line.strip_prefix("        ")
            && !rest.starts_with('/')
            && !rest.starts_with('#')
            && let Some((field, _)) = rest.split_once(':')
            && field.chars().all(|c| c.is_ascii_lowercase() || c == '_')
        {
            fields.insert(field.to_owned());
        }
    }
    if variants.is_empty() {
        return Err(String::from("no variants parsed from the Event enum"));
    }
    Ok(variants)
}

/// `(heading, section body)` for every `### ` section of the reference.
fn documented_sections(reference: &str) -> Vec<(String, String)> {
    let mut sections = Vec::new();
    let mut current: Option<(String, String)> = None;
    for line in reference.lines() {
        if let Some(heading) = line.strip_prefix("### ") {
            if let Some(done) = current.take() {
                sections.push(done);
            }
            current = Some((heading.trim().to_owned(), String::new()));
        } else if let Some((_, body)) = current.as_mut() {
            body.push_str(line);
            body.push('\n');
        }
    }
    if let Some(done) = current {
        sections.push(done);
    }
    sections
}

#[test]
fn the_event_reference_names_every_variant_and_every_field() -> Result<(), String> {
    let root = repo_root();
    let source = fs::read_to_string(root.join("crates/aion-core/src/event.rs"))
        .map_err(|error| format!("event.rs unreadable: {error}"))?;
    let reference = fs::read_to_string(root.join("docs/reference/events.md")).map_err(|error| {
        format!(
            "docs/reference/events.md unreadable ({error}); run scripts/render-events-reference.py"
        )
    })?;
    let declared = declared_variants(&source)?;
    let documented = documented_sections(&reference);

    let declared_names: BTreeSet<&str> = declared.iter().map(|(name, _)| name.as_str()).collect();
    let documented_names: BTreeSet<&str> =
        documented.iter().map(|(name, _)| name.as_str()).collect();
    let mut drift = Vec::new();
    for missing in declared_names.difference(&documented_names) {
        drift.push(format!(
            "variant `{missing}` is declared but has no section"
        ));
    }
    for stale in documented_names.difference(&declared_names) {
        drift.push(format!("section `{stale}` names no declared variant"));
    }
    for (name, fields) in &declared {
        if let Some((_, body)) = documented.iter().find(|(heading, _)| heading == name) {
            for field in fields {
                if !body.contains(&format!("| `{field}` |")) {
                    drift.push(format!(
                        "variant `{name}` field `{field}` is not in its table"
                    ));
                }
            }
        }
    }
    let declared_order: Vec<&str> = declared.iter().map(|(name, _)| name.as_str()).collect();
    let documented_order: Vec<&str> = documented.iter().map(|(name, _)| name.as_str()).collect();
    if drift.is_empty() && declared_order != documented_order {
        drift.push(String::from("sections are not in declaration order"));
    }
    if drift.is_empty() {
        Ok(())
    } else {
        Err(format!(
            "docs/reference/events.md has drifted from aion_core::Event; run \
             `python3 scripts/render-events-reference.py`:\n  {}",
            drift.join("\n  ")
        ))
    }
}