1use indexmap::IndexSet;
7use serde_json::Value;
8
9#[derive(Debug, Clone)]
11pub struct StateChange {
12 pub changed_paths: IndexSet<String>,
14}
15
16impl StateChange {
17 pub fn new() -> Self {
18 Self {
19 changed_paths: IndexSet::new(),
20 }
21 }
22
23 pub fn add_path(&mut self, path: impl Into<String>) {
25 self.changed_paths.insert(path.into());
26 }
27
28 pub fn from_json(patch: &Value) -> Self {
31 let mut change = Self::new();
32 extract_paths("", patch, &mut change.changed_paths);
33 change
34 }
35
36 pub fn from_paths(paths: impl IntoIterator<Item = String>) -> Self {
38 Self {
39 changed_paths: paths.into_iter().collect(),
40 }
41 }
42
43 pub fn paths(&self) -> impl Iterator<Item = &str> {
45 self.changed_paths.iter().map(|s| s.as_str())
46 }
47
48 pub fn contains(&self, path: &str) -> bool {
50 self.changed_paths.contains(path)
51 }
52
53 pub fn has_prefix(&self, prefix: &str) -> bool {
56 self.changed_paths.iter().any(|p| {
57 p == prefix || p.starts_with(&format!("{}.", prefix))
58 })
59 }
60}
61
62impl Default for StateChange {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68fn extract_paths(prefix: &str, value: &Value, paths: &mut IndexSet<String>) {
70 match value {
71 Value::Object(map) => {
72 if !prefix.is_empty() {
74 paths.insert(prefix.to_string());
75 }
76
77 for (key, val) in map {
79 let path = if prefix.is_empty() {
80 key.clone()
81 } else {
82 format!("{}.{}", prefix, key)
83 };
84 extract_paths(&path, val, paths);
85 }
86 }
87 _ => {
88 if !prefix.is_empty() {
90 paths.insert(prefix.to_string());
91 }
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use serde_json::json;
100
101 #[test]
102 fn test_extract_paths() {
103 let value = json!({
104 "user": {
105 "name": "Alice",
106 "age": 30
107 },
108 "count": 42
109 });
110
111 let change = StateChange::from_json(&value);
112
113 assert!(change.contains("user"));
114 assert!(change.contains("user.name"));
115 assert!(change.contains("user.age"));
116 assert!(change.contains("count"));
117 }
118
119 #[test]
120 fn test_has_prefix() {
121 let mut change = StateChange::new();
122 change.add_path("user.name");
123 change.add_path("user.profile.bio");
124 change.add_path("count");
125
126 assert!(change.has_prefix("user"));
128
129 assert!(change.has_prefix("user.profile"));
131
132 assert!(change.has_prefix("count"));
134
135 assert!(!change.has_prefix("settings"));
137 }
138
139 #[test]
140 fn test_state_change_from_paths() {
141 let paths = vec!["user.name".to_string(), "count".to_string()];
142 let change = StateChange::from_paths(paths);
143
144 assert_eq!(change.changed_paths.len(), 2);
145 assert!(change.contains("user.name"));
146 assert!(change.contains("count"));
147 }
148}