sim-citizen 0.1.2

Citizen support outside the SIM kernel.
Documentation
//! Renders the generated census of registered citizens as Markdown.

use crate::{
    CitizenInfo, CitizenRegistry, NonCitizenInfo, registered_citizens, registered_non_citizens,
};

/// Renders the full census of inventory-registered citizens as Markdown.
///
/// Collects every [`CitizenInfo`] registered through `inventory`, sorts by
/// symbol then crate, and delegates to [`render_citizen_census`]. When explicit
/// non-citizen exemptions are registered, appends their generated census
/// section too.
pub fn citizen_census_markdown() -> String {
    let citizens = sorted_citizens();
    let exemptions = sorted_non_citizens();
    let mut out = render_citizen_census(&citizens);
    if !exemptions.is_empty() {
        out.push('\n');
        out.push_str(&render_non_citizen_census(&exemptions));
    }
    out
}

/// Renders the supplied explicit registry's citizens as Markdown.
///
/// This is the DCE-safe census path for callers that build a
/// [`CitizenRegistry`] by naming each citizen type they expect.
pub fn citizen_registry_census_markdown(registry: &CitizenRegistry) -> String {
    let citizens = sorted_registry_citizens(registry);
    render_citizen_census(&citizens)
}

/// Renders the supplied citizen rows as a generated Markdown census table.
///
/// Emits a fixed header plus one table row per [`CitizenInfo`]. Callers pass an
/// already-ordered slice; this function does not sort.
pub fn render_citizen_census(citizens: &[&CitizenInfo]) -> String {
    let mut out = String::new();
    out.push_str("# Generated Citizen Census\n\n");
    out.push_str("Generated by `cargo test --workspace --all-features citizen`.\n");
    out.push_str("Do not edit by hand.\n\n");
    out.push_str(&format!("Total citizens: {}\n\n", citizens.len()));
    out.push_str("| Symbol | Version | Arity | Crate |\n");
    out.push_str("| --- | ---: | ---: | --- |\n");
    for info in citizens {
        out.push_str(&format!(
            "| `{}` | {} | {} | `{}` |\n",
            info.symbol, info.version, info.arity, info.crate_name
        ));
    }
    out
}

/// Renders the full census of inventory-registered non-citizen exemptions as
/// Markdown.
pub fn non_citizen_census_markdown() -> String {
    let exemptions = sorted_non_citizens();
    render_non_citizen_census(&exemptions)
}

/// Renders the supplied non-citizen exemption rows as a generated Markdown
/// census table.
pub fn render_non_citizen_census(exemptions: &[&NonCitizenInfo]) -> String {
    let mut out = String::new();
    out.push_str("# Generated Non-Citizen Exemption Census\n\n");
    out.push_str("Generated by `cargo test --workspace --all-features citizen`.\n");
    out.push_str("Do not edit by hand.\n\n");
    out.push_str(&format!("Total exemptions: {}\n\n", exemptions.len()));
    out.push_str("| Type | Kind | Descriptor | Crate | Reason |\n");
    out.push_str("| --- | --- | --- | --- | --- |\n");
    for info in exemptions {
        out.push_str(&format!(
            "| `{}` | `{}` | `{}` | `{}` | {} |\n",
            info.type_name, info.kind, info.descriptor, info.crate_name, info.reason
        ));
    }
    out
}

fn sorted_citizens() -> Vec<&'static CitizenInfo> {
    let mut citizens = registered_citizens().collect::<Vec<_>>();
    citizens.sort_by(|left, right| {
        left.symbol
            .cmp(right.symbol)
            .then_with(|| left.crate_name.cmp(right.crate_name))
    });
    citizens
}

fn sorted_registry_citizens(registry: &CitizenRegistry) -> Vec<&CitizenInfo> {
    let mut citizens = registry.citizens().collect::<Vec<_>>();
    citizens.sort_by(|left, right| {
        left.symbol
            .cmp(right.symbol)
            .then_with(|| left.crate_name.cmp(right.crate_name))
    });
    citizens
}

fn sorted_non_citizens() -> Vec<&'static NonCitizenInfo> {
    let mut exemptions = registered_non_citizens().collect::<Vec<_>>();
    exemptions.sort_by(|left, right| {
        left.type_name
            .cmp(right.type_name)
            .then_with(|| left.crate_name.cmp(right.crate_name))
    });
    exemptions
}