agentguard_policy/
diff.rs1use crate::PolicyBundle;
4
5pub fn diff_bundles(old: &PolicyBundle, new: &PolicyBundle) -> Vec<(String, String)> {
10 use std::collections::HashMap;
11 let old_map: HashMap<&str, &str> = old
12 .policies
13 .iter()
14 .map(|p| (p.id.as_str(), p.source.as_str()))
15 .collect();
16 let new_map: HashMap<&str, &str> = new
17 .policies
18 .iter()
19 .map(|p| (p.id.as_str(), p.source.as_str()))
20 .collect();
21
22 let mut out: Vec<(String, String)> = Vec::new();
23
24 for id in old_map.keys() {
26 if !new_map.contains_key(*id) {
27 out.push(((*id).to_string(), line_diff(Some(""), Some(""))));
28 }
29 }
30 for (id, new_src) in &new_map {
32 let old_src = old_map.get(id).copied();
33 if old_src != Some(*new_src) {
34 out.push(((*id).to_string(), line_diff(old_src, Some(new_src))));
35 }
36 }
37 out
38}
39
40pub fn line_diff(old: Option<&str>, new: Option<&str>) -> String {
42 use similar::{ChangeTag, TextDiff};
43 let old = old.unwrap_or("");
44 let new = new.unwrap_or("");
45 let diff = TextDiff::from_lines(old, new);
46 let mut out = String::new();
47 for change in diff.iter_all_changes() {
48 let prefix = match change.tag() {
49 ChangeTag::Equal => " ",
50 ChangeTag::Insert => "+",
51 ChangeTag::Delete => "-",
52 };
53 out.push_str(prefix);
54 out.push_str(change.value());
55 if !change.value().ends_with('\n') {
56 out.push('\n');
57 }
58 }
59 out
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::version::PolicyVersion;
66 use crate::{NamedPolicy, PolicyBundle};
67
68 fn bundle(id: &str, source: &str) -> PolicyBundle {
69 PolicyBundle {
70 version: PolicyVersion::new(1),
71 tenant_id: "t".into(),
72 schema_hash: [0u8; 32],
73 policies_hash: [0u8; 32],
74 created_at: 0,
75 created_by: "test".into(),
76 signature: None,
77 schema_source: "entity User;".into(),
78 policies: vec![NamedPolicy {
79 id: id.into(),
80 source: source.into(),
81 }],
82 }
83 }
84
85 #[test]
86 fn unchanged_policies_produce_no_diff() {
87 let b = bundle("p0", "permit (principal, action, resource);");
88 let diff = diff_bundles(&b, &b);
89 assert!(diff.is_empty(), "expected no diff, got {:?}", diff);
90 }
91
92 #[test]
93 fn changed_policy_is_detected() {
94 let a = bundle("p0", "permit (principal, action, resource);");
95 let b = bundle("p0", "forbid (principal, action, resource);");
96 let diff = diff_bundles(&a, &b);
97 assert_eq!(diff.len(), 1);
98 let (id, text) = &diff[0];
99 assert_eq!(id, "p0");
100 assert!(text.contains("-permit"));
101 assert!(text.contains("+forbid"));
102 }
103
104 #[test]
105 fn added_policy_is_detected() {
106 let a = bundle("p0", "permit (principal, action, resource);");
107 let mut b = bundle("p0", "permit (principal, action, resource);");
108 b.policies.push(NamedPolicy {
109 id: "p1".into(),
110 source: "permit (...)".into(),
111 });
112 let diff = diff_bundles(&a, &b);
113 assert!(diff.iter().any(|(id, _)| id == "p1"));
114 }
115}