//! Read-only audit presentation; persisted event payloads are never rewritten. use crate::domain::WorkspaceDocument; use serde_json::{Value, json}; use std::collections::BTreeMap; pub type Names = BTreeMap; pub fn remember(names: &mut Names, doc: &WorkspaceDocument) { for person in &doc.people { names.insert(person.id.to_string(), person.display_name.clone()); for alias in &person.aliases { names.insert(alias.to_string(), person.display_name.clone()); } if let Some(id) = person.user_id { names.insert(id.to_string(), person.display_name.clone()); } } for (id, name) in doc .schedules .iter() .map(|s| (s.id, &s.name)) .chain(doc.escalations.iter().map(|s| (s.id, &s.name))) .chain(doc.routes.iter().map(|s| (s.id, &s.name))) .chain(doc.generation.rules.iter().map(|s| (s.id, &s.name))) .chain(doc.generation.variables.iter().map(|s| (s.id, &s.name))) { names.insert(id.to_string(), name.clone()); } } pub fn resolve(value: &Value, names: &Names) -> Value { match value { Value::String(id) => names .get(id) .map(|name| json!({"name":name,"id":id})) .unwrap_or_else(|| value.clone()), Value::Array(items) => Value::Array(items.iter().map(|v| resolve(v, names)).collect()), Value::Object(fields) => Value::Object( fields .iter() .map(|(key, value)| { // Free text must remain verbatim, even if a comment happens to equal a UUID. let reference = key == "id" || key.ends_with("_id") || matches!( key.as_str(), "from_user" | "to_user" | "requested_by" | "forced_by" | "approvals" | "required_approvers" | "participants" | "primary" | "secondary" | "fallback" | "user" | "redirect_to" | "escalation" | "variable" | "iteration" | "output" ); let resolved = if reference || value.is_object() || value.is_array() { resolve(value, names) } else { value.clone() }; (key.clone(), resolved) }) .collect(), ), _ => value.clone(), } } #[cfg(test)] mod tests { use super::*; #[test] fn references_expand_without_changing_comments_unknown_ids_or_raw_details() { let id = uuid::Uuid::new_v4().to_string(); let names = Names::from([(id.clone(), "Alice".into())]); let raw = json!({"from_user":id,"approvals":[id],"reason":id,"unknown_id":"missing","legs":[{"to_user":id}]}); let resolved = resolve(&raw, &names); assert_eq!(resolved["from_user"]["name"], "Alice"); assert_eq!(resolved["approvals"][0]["id"], id); assert_eq!(resolved["legs"][0]["to_user"]["name"], "Alice"); assert_eq!(resolved["reason"], id); assert_eq!(resolved["unknown_id"], "missing"); assert_eq!(raw["from_user"], id); } #[test] fn merged_contact_ids_keep_resolving_to_the_person() { let (old, new) = (uuid::Uuid::new_v4(), uuid::Uuid::new_v4()); let mut doc = WorkspaceDocument::new("Team".into()); doc.people.push(crate::domain::Person { id: new, user_id: Some(new), display_name: "Dana".into(), email: Some("dana@example.com".into()), country: None, aliases: vec![old], }); let mut names = Names::new(); remember(&mut names, &doc); assert_eq!(names[&old.to_string()], "Dana"); assert_eq!(names[&new.to_string()], "Dana"); } }