//! `build_ir()` renders the proc-macro-populated registries; the emitted KDL //! is parsed by the `kdl` crate and then compared byte for byte with //! `protocol/mizan-codegen/tests/fixtures/afi_ir.kdl`. use kdl::{KdlDocument, KdlNode}; use mizan_core as mizan; use mizan_core::prelude::*; use mizan_core::RequestHandle; use serde::{Deserialize, Serialize}; use std::path::PathBuf; // ─── Output / shared types ────────────────────────────────────────────────── #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct EchoOutput { pub message: String, } #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct WhoamiOutput { pub email: String, pub authenticated: bool, } #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct ProfileOutput { pub user_id: i64, pub name: String, } #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct OrderOutput { pub id: i64, pub user_id: i64, pub total: i64, } #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct StatusOutput { pub ok: bool, } #[mizan::context("user")] pub struct UserCtx; // ─── Fixture handlers ─────────────────────────────────────────────────────── /// `(order id, owning user id, total)` — the store the order handlers read. const ORDERS: &[(i64, i64, i64)] = &[(10, 1, 4200), (11, 1, 1750), (12, 2, 990)]; fn profile_of(user_id: i64) -> ProfileOutput { ProfileOutput { user_id, name: format!("user-{user_id}"), } } #[mizan::client] pub async fn echo(_req: &RequestHandle<'_>, text: String) -> EchoOutput { EchoOutput { message: format!("echo: {text}"), } } #[mizan::client] pub async fn whoami(_req: &RequestHandle<'_>) -> WhoamiOutput { WhoamiOutput { email: "anon@example.com".into(), authenticated: false, } } #[mizan::client(context = UserCtx)] pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { profile_of(user_id) } #[mizan::client(context = UserCtx)] pub async fn user_orders(_req: &RequestHandle<'_>, user_id: i64) -> Vec { ORDERS .iter() .filter(|(_, owner, _)| *owner == user_id) .map(|(id, owner, total)| OrderOutput { id: *id, user_id: *owner, total: *total, }) .collect() } #[mizan::client(affects = UserCtx)] pub async fn update_profile( _req: &RequestHandle<'_>, user_id: i64, name: String, ) -> StatusOutput { StatusOutput { ok: user_id > 0 && !name.trim().is_empty(), } } #[mizan::client] pub async fn find_user(_req: &RequestHandle<'_>, user_id: i64) -> Option { ORDERS .iter() .any(|(_, owner, _)| *owner == user_id) .then(|| profile_of(user_id)) } #[mizan::client(merge = UserCtx)] pub async fn rename_user( _req: &RequestHandle<'_>, user_id: i64, name: String, ) -> ProfileOutput { ProfileOutput { user_id, name } } // ─── Reading the parsed document ──────────────────────────────────────────── fn canonical_kdl_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../protocol/mizan-codegen/tests/fixtures/afi_ir.kdl") } /// The node's first string argument, or the empty string when it has none. fn label(node: &KdlNode) -> String { for entry in node.entries() { if let Some(s) = entry.value().as_string() { return s.to_string(); } } String::new() } /// `(node name, first string argument)` for every node at one level. fn index(nodes: &[KdlNode]) -> Vec<(String, String)> { nodes .iter() .map(|node| (node.name().value().to_string(), label(node))) .collect() } /// The child nodes of the first `kind "name"` node in `doc`, or an empty slice /// when the document has no such node or it carries no child block. fn children_of<'a>(doc: &'a KdlDocument, kind: &str, name: &str) -> &'a [KdlNode] { for node in doc.nodes() { if node.name().value() == kind && label(node) == name { return match node.children() { Some(block) => block.nodes(), None => &[], }; } } &[] } #[test] fn build_ir_matches_canonical_afi_kdl() { let emitted = mizan_core::build_ir(); // Parsing before comparing means a malformed emission fails here rather // than as a confusing textual diff. let parsed: KdlDocument = emitted .parse() .expect("build_ir() output is a well-formed KDL document"); let top = index(parsed.nodes()); assert!( top.contains(&("function".to_string(), "user_orders".to_string())), "parsed document is missing the user_orders function node: {top:?}", ); assert!( top.contains(&("context".to_string(), "user".to_string())), "parsed document is missing the user context node: {top:?}", ); assert_eq!( index(children_of(&parsed, "function", "user_orders")), vec![ ("camel".to_string(), "userOrders".to_string()), ("has-input".to_string(), String::new()), ("input".to_string(), "userOrdersInput".to_string()), ("output".to_string(), "userOrdersOutput".to_string()), ("transport".to_string(), "http".to_string()), ("context".to_string(), "user".to_string()), ], ); assert_eq!( index(children_of(&parsed, "context", "user")), vec![ ("function".to_string(), "user_orders".to_string()), ("function".to_string(), "user_profile".to_string()), ("param".to_string(), "user_id".to_string()), ], ); let expected = std::fs::read_to_string(canonical_kdl_path()).expect("read canonical KDL"); let canonical: KdlDocument = expected .parse() .expect("the canonical fixture is a well-formed KDL document"); assert_eq!( index(parsed.nodes()), index(canonical.nodes()), "emitted and canonical documents declare different top-level nodes", ); if emitted != expected { for (lineno, (a, b)) in emitted.lines().zip(expected.lines()).enumerate() { if a != b { panic!( "KDL diverges at line {}:\n expected: {b:?}\n actual: {a:?}", lineno + 1, ); } } panic!( "KDL diverges in length: actual_len={} expected_len={}", emitted.len(), expected.len(), ); } }