1use serde::{de::DeserializeOwned, Serialize};
2use serde_json::Value;
3
4use crate::error::{Result, SdkError};
5
6pub trait State: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {}
24
25impl<T> State for T where T: Serialize + DeserializeOwned + Clone + Send + Sync + 'static {}
27
28pub(crate) struct StateContainer<S: State> {
30 value: S,
32 snapshot: Value,
35}
36
37impl<S: State> StateContainer<S> {
38 pub fn new(initial: S) -> Result<Self> {
40 let snapshot =
41 serde_json::to_value(&initial).map_err(|e| SdkError::StateSerde(e.to_string()))?;
42 Ok(Self {
43 value: initial,
44 snapshot,
45 })
46 }
47
48 pub fn get(&self) -> &S {
50 &self.value
51 }
52
53 pub fn get_mut(&mut self) -> &mut S {
55 &mut self.value
56 }
57
58 pub fn take_snapshot(&mut self) -> Result<()> {
60 self.snapshot =
61 serde_json::to_value(&self.value).map_err(|e| SdkError::StateSerde(e.to_string()))?;
62 Ok(())
63 }
64
65 pub fn changed_paths(&self) -> Result<Vec<String>> {
75 let current =
76 serde_json::to_value(&self.value).map_err(|e| SdkError::StateSerde(e.to_string()))?;
77 Ok(hypen_engine::diff_paths(&self.snapshot, ¤t)
78 .into_iter()
79 .map(|e| e.path)
80 .collect())
81 }
82
83 pub fn to_json(&self) -> Result<Value> {
85 serde_json::to_value(&self.value).map_err(|e| SdkError::StateSerde(e.to_string()))
86 }
87
88 pub fn diff_patch(&self) -> Result<Value> {
103 let current =
104 serde_json::to_value(&self.value).map_err(|e| SdkError::StateSerde(e.to_string()))?;
105 let mut patch = serde_json::Map::new();
106 let Value::Object(current_map) = ¤t else {
107 return Ok(Value::Object(patch));
108 };
109 for entry in hypen_engine::diff_paths(&self.snapshot, ¤t) {
110 let root = match entry.path.split_once('.') {
111 Some((head, _)) => head,
112 None => entry.path.as_str(),
113 };
114 if patch.contains_key(root) {
115 continue;
116 }
117 if let Some(new_subtree) = current_map.get(root) {
118 patch.insert(root.to_string(), new_subtree.clone());
119 }
120 }
121 Ok(Value::Object(patch))
122 }
123}
124
125pub(crate) fn apply_bind<S: State>(current: &S, path: &str, value: Value) -> Result<S> {
142 let mut json =
143 serde_json::to_value(current).map_err(|e| SdkError::StateSerde(e.to_string()))?;
144 hypen_engine::path_set(&mut json, path, value);
145 serde_json::from_value(json)
146 .map_err(|e| SdkError::StateSerde(format!("__hypen_bind apply at '{path}': {e}")))
147}
148
149pub(crate) fn apply_bind_to_json<S: State>(
153 state_json: &Value,
154 path: &str,
155 value: Value,
156) -> Option<Value> {
157 let mut new_json = state_json.clone();
158 hypen_engine::path_set(&mut new_json, path, value);
159 let typed: S = serde_json::from_value(new_json.clone()).ok()?;
160 serde_json::to_value(&typed).ok()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use serde::{Deserialize, Serialize};
167 use serde_json::json;
168
169 #[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
170 struct TestState {
171 count: i32,
172 name: String,
173 items: Vec<String>,
174 }
175
176 #[test]
177 fn test_diff_no_change() {
178 let container = StateContainer::new(TestState {
179 count: 0,
180 name: "Alice".into(),
181 items: vec![],
182 })
183 .unwrap();
184
185 let paths = container.changed_paths().unwrap();
186 assert!(paths.is_empty());
187 }
188
189 #[test]
190 fn test_diff_scalar_change() {
191 let mut container = StateContainer::new(TestState {
192 count: 0,
193 name: "Alice".into(),
194 items: vec![],
195 })
196 .unwrap();
197
198 container.take_snapshot().unwrap();
199 container.get_mut().count = 42;
200
201 let paths = container.changed_paths().unwrap();
202 assert_eq!(paths, vec!["count"]);
203 }
204
205 #[test]
206 fn test_diff_multiple_changes() {
207 let mut container = StateContainer::new(TestState {
208 count: 0,
209 name: "Alice".into(),
210 items: vec![],
211 })
212 .unwrap();
213
214 container.take_snapshot().unwrap();
215 container.get_mut().count = 10;
216 container.get_mut().name = "Bob".into();
217
218 let mut paths = container.changed_paths().unwrap();
219 paths.sort();
220 assert_eq!(paths, vec!["count", "name"]);
221 }
222
223 #[test]
224 fn test_diff_array_change() {
225 let mut container = StateContainer::new(TestState {
226 count: 0,
227 name: "Alice".into(),
228 items: vec!["a".into()],
229 })
230 .unwrap();
231
232 container.take_snapshot().unwrap();
233 container.get_mut().items.push("b".into());
234
235 let paths = container.changed_paths().unwrap();
236 assert!(paths.contains(&"items.1".to_string()));
237 }
238
239 #[test]
240 fn test_diff_nested_struct() {
241 #[derive(Clone, Default, Serialize, Deserialize)]
242 struct Nested {
243 user: User,
244 count: i32,
245 }
246
247 #[derive(Clone, Default, Serialize, Deserialize)]
248 struct User {
249 name: String,
250 age: i32,
251 }
252
253 let mut container = StateContainer::new(Nested {
254 user: User {
255 name: "Alice".into(),
256 age: 30,
257 },
258 count: 0,
259 })
260 .unwrap();
261
262 container.take_snapshot().unwrap();
263 container.get_mut().user.age = 31;
264
265 let paths = container.changed_paths().unwrap();
266 assert_eq!(paths, vec!["user.age"]);
267 }
268
269 #[test]
270 fn test_diff_delegates_to_engine() {
271 let old = json!({"a": 1, "b": {"c": 2, "d": 3}});
275 let new = json!({"a": 1, "b": {"c": 99, "d": 3}, "e": true});
276 let paths: Vec<String> = hypen_engine::diff_paths(&old, &new)
277 .into_iter()
278 .map(|e| e.path)
279 .collect();
280 assert!(paths.contains(&"b.c".to_string()));
281 assert!(paths.contains(&"e".to_string()));
282 assert!(!paths.contains(&"a".to_string()));
283 assert!(!paths.contains(&"b.d".to_string()));
284 }
285
286 #[test]
287 fn test_diff_patch_output() {
288 let mut container = StateContainer::new(TestState {
289 count: 0,
290 name: "Alice".into(),
291 items: vec![],
292 })
293 .unwrap();
294
295 container.take_snapshot().unwrap();
296 container.get_mut().count = 5;
297
298 let patch = container.diff_patch().unwrap();
299 assert_eq!(patch, json!({"count": 5}));
300 }
301
302 #[test]
303 fn diff_patch_rolls_up_growing_vec_to_root_subtree() {
304 let mut container = StateContainer::new(TestState {
312 count: 0,
313 name: "x".into(),
314 items: vec![],
315 })
316 .unwrap();
317 container.take_snapshot().unwrap();
318 container.get_mut().items.push("first".into());
319
320 let patch = container.diff_patch().unwrap();
321 assert_eq!(
322 patch,
323 json!({"items": ["first"]}),
324 "growing vec must emit whole `items` subtree, not `items.0`",
325 );
326 }
327
328 #[test]
329 fn diff_patch_rolls_up_nested_object_change_to_root() {
330 #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug)]
335 struct Profile {
336 name: String,
337 }
338 #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug)]
339 struct User {
340 profile: Profile,
341 age: i32,
342 }
343 #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug)]
344 struct S {
345 user: User,
346 }
347
348 let mut container = StateContainer::new(S {
349 user: User {
350 profile: Profile { name: "old".into() },
351 age: 30,
352 },
353 })
354 .unwrap();
355 container.take_snapshot().unwrap();
356 container.get_mut().user.profile.name = "new".into();
357
358 let patch = container.diff_patch().unwrap();
359 assert_eq!(
360 patch,
361 json!({"user": {"profile": {"name": "new"}, "age": 30}}),
362 );
363 }
364
365 #[test]
366 fn test_to_json() {
367 let container = StateContainer::new(TestState {
368 count: 42,
369 name: "Bob".into(),
370 items: vec!["x".into()],
371 })
372 .unwrap();
373
374 let json = container.to_json().unwrap();
375 assert_eq!(json["count"], 42);
376 assert_eq!(json["name"], "Bob");
377 }
378}