kryphocron 0.3.0

Privacy-first ATProto substrate primitives: type architecture, audit vocabulary, inter-service auth, and encryption hook surfaces
//! Build script — KRYPHOCRON_CRATE_DESIGN.md §5.4 post-processing.
//!
//! Emits the per-record-type `HasNsid` + `sealed::Sealed` impls that the
//! §5.4 codegen pipeline always specified but that was never created when
//! the crate first shipped. Source of truth is the kryphocron-lexicons
//! `KRYPHOCRON_LEXICON_REGISTRY` (NSID -> tier), read here via a
//! build-dependency on that crate. The record types are the
//! proto-blue-codegen `kryphocron_lexicons::tools::*::Main` structs,
//! referenced by their NSID-derived module path. Orphan rules require the
//! impls to live in this crate (local trait `HasNsid` x foreign record
//! types), which is why this post-processing is here and not in
//! kryphocron-lexicons.
//!
//! Output `OUT_DIR/has_nsid_impls.rs` is `include!`d from `src/tier.rs`:
//!   - one `impl crate::sealed::Sealed` + `impl crate::tier::HasNsid` per
//!     registry entry (tier from the entry);
//!   - `KRYPHOCRON_IMPLEMENTED_NSIDS` (sorted);
//!   - a compile-time §5.3 consistency assertion against
//!     `KRYPHOCRON_LEXICON_REGISTRY`.

use std::fmt::Write as _;
use std::{env, fs, path::PathBuf};

use kryphocron_lexicons::{LexiconRegistryEntry, Tier, KRYPHOCRON_LEXICON_REGISTRY};

/// NSID -> proto-blue-codegen module path for the record type, snake-casing
/// each segment (`tools.kryphocron.feed.postPrivate` ->
/// `tools::kryphocron::feed::post_private`).
fn record_module_path(nsid: &str) -> String {
    nsid.split('.')
        .map(snake_case_segment)
        .collect::<Vec<_>>()
        .join("::")
}

fn snake_case_segment(segment: &str) -> String {
    let mut out = String::with_capacity(segment.len() + 4);
    for ch in segment.chars() {
        if ch.is_ascii_uppercase() {
            out.push('_');
            out.push(ch.to_ascii_lowercase());
        } else {
            out.push(ch);
        }
    }
    out
}

fn main() {
    // Deterministic output: sort registry entries by NSID.
    let mut entries: Vec<&LexiconRegistryEntry> = KRYPHOCRON_LEXICON_REGISTRY.iter().collect();
    entries.sort_by_key(|e| e.nsid);

    let mut src = String::new();
    src.push_str("// @generated by kryphocron/build.rs (§5.4 post-processing). Do not edit.\n\n");

    for e in &entries {
        let ty = format!("::kryphocron_lexicons::{}::Main", record_module_path(e.nsid));
        let witness = match e.tier {
            Tier::Public => "PublicTier",
            Tier::Private => "PrivateTier",
            // `Tier` is #[non_exhaustive]; a new variant ships with its own
            // witness. Fail the build loudly rather than silently mis-tier.
            other => panic!(
                "kryphocron build.rs: NSID `{}` has unhandled tier {other:?} — \
                 add a tier-witness mapping for the new tier",
                e.nsid
            ),
        };
        writeln!(src, "impl crate::sealed::Sealed for {ty} {{}}").unwrap();
        writeln!(
            src,
            "impl crate::tier::HasNsid for {ty} {{\n    \
             const NSID: &'static str = {:?};\n    \
             type Tier = crate::tier::{witness};\n}}\n",
            e.nsid
        )
        .unwrap();
    }

    src.push_str(
        "/// NSIDs for which the §5.4 build post-processing emitted a `HasNsid`\n\
         /// impl — one per record type, sorted. Checked for set-equality against\n\
         /// [`crate::KRYPHOCRON_LEXICON_REGISTRY`] (§5.3 bidirectional consistency).\n\
         pub const KRYPHOCRON_IMPLEMENTED_NSIDS: &[&str] = &[\n",
    );
    for e in &entries {
        writeln!(src, "    {:?},", e.nsid).unwrap();
    }
    src.push_str("];\n\n");

    // §5.3 bidirectional consistency. The impl set is derived one-to-one from
    // the registry above, so a length match with the registry is a
    // compile-time guarantee that no entry was dropped or duplicated. (Drift
    // in the other direction — a registry NSID whose record type is missing —
    // is caught earlier still: the generated `impl ... for ...::Main` fails to
    // compile if the type does not exist.)
    src.push_str(
        "const _: () = {\n    \
         assert!(\n        \
         KRYPHOCRON_IMPLEMENTED_NSIDS.len()\n            \
         == ::kryphocron_lexicons::KRYPHOCRON_LEXICON_REGISTRY.len(),\n        \
         \"HasNsid impl set / lexicon registry size mismatch — regenerate codegen\"\n    \
         );\n};\n",
    );

    let dest = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")).join("has_nsid_impls.rs");
    fs::write(&dest, src).expect("write has_nsid_impls.rs");

    println!("cargo:rerun-if-changed=build.rs");
}