//! Decodes the AFI fixture into the typed IR structs. Each test reads back a //! different facet of the KDL — the function set, per-function field decoding, //! context-param elevation, and the named-type table. use std::path::PathBuf; use mizan_codegen::fetch::parse_ir_from_str; use mizan_codegen::ir::{AffectKind, CallInput, IsContext, Primitive, Transport}; fn load_fixture() -> mizan_codegen::ir::MizanIR { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/afi_ir.kdl"); let raw = std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("read {}: {e}", path.display())); parse_ir_from_str(&raw).unwrap_or_else(|e| panic!("parse IR: {e}")) } #[test] fn afi_fixture_deserializes_function_set() { let ir = load_fixture(); let names: Vec<&str> = ir.functions.iter().map(|f| f.name.as_str()).collect(); assert_eq!(ir.functions.len(), 7, "expected 7 functions, got {}: {names:?}", ir.functions.len()); for expected in [ "echo", "whoami", "user_profile", "user_orders", "update_profile", "find_user", "rename_user", ] { assert!(names.contains(&expected), "missing function {expected:?} in {names:?}"); } } #[test] fn afi_fixture_function_field_decode() { let ir = load_fixture(); let echo = ir.functions.iter().find(|f| f.name == "echo").unwrap(); assert_eq!(echo.camel_name, "echo"); assert_eq!(echo.input, CallInput::Typed("echoInput".to_string())); assert_eq!(echo.output_type, "echoOutput"); assert!(!echo.output_nullable); assert_eq!(echo.transport, Transport::Http); assert_eq!(echo.is_context, IsContext::No); let whoami = ir.functions.iter().find(|f| f.name == "whoami").unwrap(); assert_eq!(whoami.input, CallInput::Absent); // `find_user` returns `ProfileOutput | None` — outputNullable must be true. let find_user = ir.functions.iter().find(|f| f.name == "find_user").unwrap(); assert!(find_user.output_nullable, "find_user must be outputNullable"); // Context-typed function picks up the context name. let user_profile = ir.functions.iter().find(|f| f.name == "user_profile").unwrap(); assert_eq!(user_profile.is_context.as_str(), Some("user")); // Mutation with `affects="user"` lands in `affects` as a context target. let update_profile = ir.functions.iter().find(|f| f.name == "update_profile").unwrap(); assert_eq!(update_profile.affects.len(), 1); assert_eq!(update_profile.affects[0].kind, AffectKind::Context); assert_eq!(update_profile.affects[0].name, "user"); // Mutation with `merge="user"`. let rename_user = ir.functions.iter().find(|f| f.name == "rename_user").unwrap(); assert_eq!(rename_user.merge, vec!["user".to_string()]); } /// `has-input` and `input` are two spellings of one fact; a document that /// spells them against each other is rejected at the KDL crossing rather than /// silently reaching a target as a no-argument call. #[test] fn contradictory_input_spellings_are_rejected() { let declared_without_flag = r#" function "echo" { camel "echo" has-input #false input "echoInput" output "echoOutput" } "#; let err = parse_ir_from_str(declared_without_flag) .expect_err("`input` with `has-input #false` must not parse") .to_string(); assert!(err.contains("echoInput"), "fault must name the declared input type: {err}"); let flag_without_type = r#" function "echo" { camel "echo" has-input #true output "echoOutput" } "#; let err = parse_ir_from_str(flag_without_type) .expect_err("`has-input #true` with no `input` must not parse") .to_string(); assert!(err.contains("has-input"), "fault must name `has-input`: {err}"); } #[test] fn afi_fixture_context_param_elevation() { let ir = load_fixture(); let user = ir.contexts.get("user").expect("user context group"); // Both context functions share `user_id` as a required param. let user_id = user.params.get("user_id").expect("user_id param"); assert_eq!(user_id.ty, Primitive::Integer); assert!(user_id.required, "user_id is required (declared by every fn in the group)"); assert!(user_id.shared_by.contains(&"user_profile".to_string())); assert!(user_id.shared_by.contains(&"user_orders".to_string())); } #[test] fn afi_fixture_named_types_present() { let ir = load_fixture(); // Every function names its `Input` / `Output` type, // so the IR's type table must declare each one. for expected in [ "echoInput", "echoOutput", "whoamiOutput", "userProfileInput", "userProfileOutput", "userOrdersInput", "updateProfileInput", "updateProfileOutput", "findUserInput", "findUserOutput", "renameUserInput", "renameUserOutput", ] { assert!(ir.types.contains_key(expected), "missing type {expected:?}"); } }