//! `compute_merges` over a graph registered through `#[derive(Mizan)]`, //! `#[mizan::context]` and `#[mizan::client]`. use mizan_core as mizan; use mizan_core::prelude::*; use mizan_core::{compute_merges, RequestHandle, FUNCTIONS}; use serde::{Deserialize, Serialize}; #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct ProfileOutput { pub user_id: i64, pub name: String, } #[derive(Mizan, Serialize, Deserialize, Debug, Clone)] pub struct StatusOutput { pub ok: bool, } #[mizan::context("user")] pub struct UserCtx; #[mizan::client(context = UserCtx)] pub async fn user_profile(_req: &RequestHandle<'_>, user_id: i64) -> ProfileOutput { ProfileOutput { user_id, name: format!("user-{user_id}"), } } #[mizan::client(merge = UserCtx)] pub async fn rename_user( _req: &RequestHandle<'_>, user_id: i64, name: String, ) -> ProfileOutput { ProfileOutput { user_id, name } } #[mizan::client(affects = UserCtx)] pub async fn touch_user(_req: &RequestHandle<'_>, user_id: i64) -> StatusOutput { StatusOutput { ok: user_id > 0 } } /// The handlers above register into `FUNCTIONS` inside this test binary, so a /// name they declare always lands. fn spec(name: &str) -> &'static dyn FunctionSpec { for fn_spec in FUNCTIONS.iter().copied() { if fn_spec.name() == name { return fn_spec; } } panic!("no registered function named `{name}`"); } /// `user_id` is a declared param of the `user` context; `name` is not. fn args() -> serde_json::Map { let mut args = serde_json::Map::new(); args.insert("user_id".to_string(), serde_json::Value::from(7)); args.insert("name".to_string(), serde_json::Value::from("Renamed")); args } fn renamed() -> serde_json::Value { serde_json::json!({ "user_id": 7, "name": "Renamed" }) } #[test] fn a_merge_declaration_resolves_to_the_context_member_sharing_its_output() { let result = renamed(); let merges = compute_merges(spec("rename_user"), &args(), &result); let [entry] = merges.as_slice() else { panic!( "rename_user declares one merge; got {} entries", merges.len() ); }; assert_eq!(entry.context, "user"); assert_eq!(entry.slot, "user_profile"); assert_eq!(entry.value, result); } #[test] fn a_merge_entry_is_scoped_by_the_contexts_declared_params_alone() { let result = renamed(); let merges = compute_merges(spec("rename_user"), &args(), &result); let [entry] = merges.as_slice() else { panic!( "rename_user declares one merge; got {} entries", merges.len() ); }; let mut expected = serde_json::Map::new(); expected.insert("user_id".to_string(), serde_json::Value::from(7)); assert_eq!(entry.params, Some(expected)); } #[test] fn a_function_declaring_only_affects_produces_no_merge_entries() { let result = serde_json::json!({ "ok": true }); assert!(compute_merges(spec("touch_user"), &args(), &result).is_empty()); } #[test] fn merge_resolution_answers_identically_across_calls() { let result = renamed(); let first = compute_merges(spec("rename_user"), &args(), &result); let second = compute_merges(spec("rename_user"), &args(), &result); let slots: Vec<&str> = first.iter().map(|e| e.slot.as_str()).collect(); let again: Vec<&str> = second.iter().map(|e| e.slot.as_str()).collect(); assert_eq!(slots, again); assert_eq!(slots, vec!["user_profile"]); }