1use serde::{Deserialize, Serialize};
6
7pub fn extract_changed_paths(patch: &serde_json::Value) -> Vec<String> {
9 let mut paths = Vec::new();
10 extract_paths_recursive(patch, String::new(), &mut paths);
11 paths
12}
13
14fn extract_paths_recursive(value: &serde_json::Value, prefix: String, paths: &mut Vec<String>) {
15 match value {
16 serde_json::Value::Object(map) => {
17 for (key, val) in map {
18 let path = if prefix.is_empty() {
19 key.clone()
20 } else {
21 format!("{}.{}", prefix, key)
22 };
23 paths.push(path.clone());
24 extract_paths_recursive(val, path, paths);
25 }
26 }
27 _ => {}
30 }
31}
32
33#[derive(Debug, Serialize, Deserialize)]
35#[serde(tag = "status")]
36pub enum FfiResult<T> {
37 #[serde(rename = "ok")]
38 Ok { value: T },
39 #[serde(rename = "error")]
40 Error { message: String },
41}
42
43impl<T> From<Result<T, String>> for FfiResult<T> {
44 fn from(result: Result<T, String>) -> Self {
45 match result {
46 Ok(value) => FfiResult::Ok { value },
47 Err(message) => FfiResult::Error { message },
48 }
49 }
50}
51
52#[derive(Debug, Serialize, Deserialize)]
54pub struct ModuleConfig {
55 pub name: String,
56 pub actions: Vec<String>,
57 pub state_keys: Vec<String>,
58 pub initial_state: serde_json::Value,
59}
60
61#[derive(Debug, Serialize, Deserialize)]
63pub struct ResolvedComponent {
64 pub source: String,
65 pub path: String,
66 #[serde(default)]
67 pub passthrough: bool,
68 #[serde(default)]
69 pub lazy: bool,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
74pub struct ActionPayload {
75 pub name: String,
76 #[serde(default)]
77 pub payload: serde_json::Value,
78}
79
80#[derive(Debug, Serialize, Deserialize)]
82pub struct SparseStateUpdate {
83 pub paths: Vec<String>,
84 pub values: serde_json::Value,
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use serde_json::json;
91
92 #[test]
97 fn test_extract_changed_paths_nested() {
98 let patch = json!({
99 "user": {
100 "name": "Alice",
101 "age": 30
102 }
103 });
104
105 let paths = extract_changed_paths(&patch);
106 assert!(paths.contains(&"user".to_string()));
107 assert!(paths.contains(&"user.name".to_string()));
108 assert!(paths.contains(&"user.age".to_string()));
109 }
110
111 #[test]
112 fn test_extract_changed_paths_flat() {
113 let patch = json!({
114 "count": 42,
115 "name": "test"
116 });
117
118 let paths = extract_changed_paths(&patch);
119 assert!(paths.contains(&"count".to_string()));
120 assert!(paths.contains(&"name".to_string()));
121 assert_eq!(paths.len(), 2);
122 }
123
124 #[test]
125 fn test_extract_changed_paths_deeply_nested() {
126 let patch = json!({
127 "a": {
128 "b": {
129 "c": {
130 "d": "value"
131 }
132 }
133 }
134 });
135
136 let paths = extract_changed_paths(&patch);
137 assert!(paths.contains(&"a".to_string()));
138 assert!(paths.contains(&"a.b".to_string()));
139 assert!(paths.contains(&"a.b.c".to_string()));
140 assert!(paths.contains(&"a.b.c.d".to_string()));
141 }
142
143 #[test]
144 fn test_extract_changed_paths_empty() {
145 let patch = json!({});
146 let paths = extract_changed_paths(&patch);
147 assert!(paths.is_empty());
148 }
149
150 #[test]
151 fn test_extract_changed_paths_primitive() {
152 let patch = json!(42);
153 let paths = extract_changed_paths(&patch);
154 assert!(paths.is_empty()); }
156
157 #[test]
158 fn test_extract_changed_paths_array() {
159 let patch = json!({
160 "items": [1, 2, 3]
161 });
162
163 let paths = extract_changed_paths(&patch);
164 assert!(paths.contains(&"items".to_string()));
165 assert_eq!(paths.len(), 1);
167 }
168
169 #[test]
170 fn test_extract_changed_paths_mixed_types() {
171 let patch = json!({
172 "string_val": "hello",
173 "number_val": 42,
174 "bool_val": true,
175 "null_val": null,
176 "array_val": [1, 2],
177 "nested": {"key": "value"}
178 });
179
180 let paths = extract_changed_paths(&patch);
181 assert!(paths.contains(&"string_val".to_string()));
182 assert!(paths.contains(&"number_val".to_string()));
183 assert!(paths.contains(&"bool_val".to_string()));
184 assert!(paths.contains(&"null_val".to_string()));
185 assert!(paths.contains(&"array_val".to_string()));
186 assert!(paths.contains(&"nested".to_string()));
187 assert!(paths.contains(&"nested.key".to_string()));
188 assert_eq!(paths.len(), 7);
189 }
190
191 #[test]
192 fn test_extract_changed_paths_special_characters_in_keys() {
193 let patch = json!({
194 "key-with-dash": 1,
195 "key_with_underscore": 2,
196 "key.with.dots": 3
197 });
198
199 let paths = extract_changed_paths(&patch);
200 assert!(paths.contains(&"key-with-dash".to_string()));
201 assert!(paths.contains(&"key_with_underscore".to_string()));
202 assert!(paths.contains(&"key.with.dots".to_string()));
203 }
204
205 #[test]
206 fn test_extract_changed_paths_unicode_keys() {
207 let patch = json!({
208 "日本語": "value",
209 "emoji🎉": "party"
210 });
211
212 let paths = extract_changed_paths(&patch);
213 assert!(paths.contains(&"日本語".to_string()));
214 assert!(paths.contains(&"emoji🎉".to_string()));
215 }
216
217 #[test]
218 fn test_extract_changed_paths_numeric_string_keys() {
219 let patch = json!({
220 "0": "first",
221 "1": "second",
222 "100": "hundredth"
223 });
224
225 let paths = extract_changed_paths(&patch);
226 assert!(paths.contains(&"0".to_string()));
227 assert!(paths.contains(&"1".to_string()));
228 assert!(paths.contains(&"100".to_string()));
229 }
230
231 #[test]
236 fn test_ffi_result_ok_serialization() {
237 let ok_result: FfiResult<i32> = FfiResult::Ok { value: 42 };
238 let json = serde_json::to_string(&ok_result).unwrap();
239 assert!(json.contains("\"status\":\"ok\""));
240 assert!(json.contains("\"value\":42"));
241 }
242
243 #[test]
244 fn test_ffi_result_error_serialization() {
245 let err_result: FfiResult<i32> = FfiResult::Error {
246 message: "Something went wrong".to_string(),
247 };
248 let json = serde_json::to_string(&err_result).unwrap();
249 assert!(json.contains("\"status\":\"error\""));
250 assert!(json.contains("Something went wrong"));
251 }
252
253 #[test]
254 fn test_ffi_result_from_result_ok() {
255 let result: Result<String, String> = Ok("success".to_string());
256 let ffi_result: FfiResult<String> = result.into();
257 match ffi_result {
258 FfiResult::Ok { value } => assert_eq!(value, "success"),
259 FfiResult::Error { .. } => panic!("Expected Ok"),
260 }
261 }
262
263 #[test]
264 fn test_ffi_result_from_result_err() {
265 let result: Result<String, String> = Err("failure".to_string());
266 let ffi_result: FfiResult<String> = result.into();
267 match ffi_result {
268 FfiResult::Ok { .. } => panic!("Expected Error"),
269 FfiResult::Error { message } => assert_eq!(message, "failure"),
270 }
271 }
272
273 #[test]
274 fn test_ffi_result_roundtrip() {
275 let original: FfiResult<Vec<i32>> = FfiResult::Ok { value: vec![1, 2, 3] };
276 let json = serde_json::to_string(&original).unwrap();
277 let parsed: FfiResult<Vec<i32>> = serde_json::from_str(&json).unwrap();
278 match parsed {
279 FfiResult::Ok { value } => assert_eq!(value, vec![1, 2, 3]),
280 FfiResult::Error { .. } => panic!("Expected Ok"),
281 }
282 }
283
284 #[test]
285 fn test_ffi_result_with_complex_value() {
286 #[derive(Debug, Serialize, Deserialize, PartialEq)]
287 struct ComplexData {
288 id: u32,
289 name: String,
290 tags: Vec<String>,
291 }
292
293 let data = ComplexData {
294 id: 123,
295 name: "test".to_string(),
296 tags: vec!["a".to_string(), "b".to_string()],
297 };
298
299 let result: FfiResult<ComplexData> = FfiResult::Ok { value: data };
300 let json = serde_json::to_string(&result).unwrap();
301 let parsed: FfiResult<ComplexData> = serde_json::from_str(&json).unwrap();
302
303 match parsed {
304 FfiResult::Ok { value } => {
305 assert_eq!(value.id, 123);
306 assert_eq!(value.name, "test");
307 assert_eq!(value.tags, vec!["a", "b"]);
308 }
309 FfiResult::Error { .. } => panic!("Expected Ok"),
310 }
311 }
312
313 #[test]
314 fn test_ffi_result_error_with_special_chars() {
315 let err: FfiResult<()> = FfiResult::Error {
316 message: "Error: \"quotes\" and 'apostrophes' and\nnewlines".to_string(),
317 };
318 let json = serde_json::to_string(&err).unwrap();
319 let parsed: FfiResult<()> = serde_json::from_str(&json).unwrap();
320
321 match parsed {
322 FfiResult::Error { message } => {
323 assert!(message.contains("quotes"));
324 assert!(message.contains("newlines"));
325 }
326 FfiResult::Ok { .. } => panic!("Expected Error"),
327 }
328 }
329
330 #[test]
335 fn test_module_config_serialization() {
336 let config = ModuleConfig {
337 name: "TestModule".to_string(),
338 actions: vec!["action1".to_string(), "action2".to_string()],
339 state_keys: vec!["count".to_string(), "name".to_string()],
340 initial_state: json!({"count": 0, "name": ""}),
341 };
342
343 let json = serde_json::to_string(&config).unwrap();
344 assert!(json.contains("\"name\":\"TestModule\""));
345 assert!(json.contains("\"actions\""));
346 assert!(json.contains("\"action1\""));
347 }
348
349 #[test]
350 fn test_module_config_deserialization() {
351 let json = r#"{
352 "name": "MyModule",
353 "actions": ["submit", "cancel"],
354 "state_keys": ["value"],
355 "initial_state": {"value": 100}
356 }"#;
357
358 let config: ModuleConfig = serde_json::from_str(json).unwrap();
359 assert_eq!(config.name, "MyModule");
360 assert_eq!(config.actions, vec!["submit", "cancel"]);
361 assert_eq!(config.state_keys, vec!["value"]);
362 assert_eq!(config.initial_state["value"], 100);
363 }
364
365 #[test]
366 fn test_module_config_empty_collections() {
367 let config = ModuleConfig {
368 name: "EmptyModule".to_string(),
369 actions: vec![],
370 state_keys: vec![],
371 initial_state: json!({}),
372 };
373
374 let json = serde_json::to_string(&config).unwrap();
375 let parsed: ModuleConfig = serde_json::from_str(&json).unwrap();
376
377 assert_eq!(parsed.name, "EmptyModule");
378 assert!(parsed.actions.is_empty());
379 assert!(parsed.state_keys.is_empty());
380 assert!(parsed.initial_state.is_object());
381 }
382
383 #[test]
384 fn test_module_config_complex_initial_state() {
385 let config = ModuleConfig {
386 name: "ComplexModule".to_string(),
387 actions: vec!["update".to_string()],
388 state_keys: vec!["user".to_string(), "items".to_string()],
389 initial_state: json!({
390 "user": {
391 "id": null,
392 "name": "",
393 "settings": {
394 "theme": "dark",
395 "notifications": true
396 }
397 },
398 "items": [],
399 "count": 0
400 }),
401 };
402
403 let json = serde_json::to_string(&config).unwrap();
404 let parsed: ModuleConfig = serde_json::from_str(&json).unwrap();
405
406 assert_eq!(parsed.initial_state["user"]["settings"]["theme"], "dark");
407 assert!(parsed.initial_state["user"]["id"].is_null());
408 assert!(parsed.initial_state["items"].is_array());
409 }
410
411 #[test]
416 fn test_resolved_component_defaults() {
417 let json = r#"{
418 "source": "Text(\"Hello\")",
419 "path": "/components/Hello.hypen"
420 }"#;
421
422 let component: ResolvedComponent = serde_json::from_str(json).unwrap();
423 assert_eq!(component.source, "Text(\"Hello\")");
424 assert_eq!(component.path, "/components/Hello.hypen");
425 assert!(!component.passthrough); assert!(!component.lazy); }
428
429 #[test]
430 fn test_resolved_component_with_flags() {
431 let component = ResolvedComponent {
432 source: "".to_string(),
433 path: "/lazy/Component.hypen".to_string(),
434 passthrough: false,
435 lazy: true,
436 };
437
438 let json = serde_json::to_string(&component).unwrap();
439 assert!(json.contains("\"lazy\":true"));
440 }
441
442 #[test]
443 fn test_resolved_component_passthrough() {
444 let component = ResolvedComponent {
445 source: String::new(),
446 path: "/components/Router.hypen".to_string(),
447 passthrough: true,
448 lazy: false,
449 };
450
451 let json = serde_json::to_string(&component).unwrap();
452 let parsed: ResolvedComponent = serde_json::from_str(&json).unwrap();
453
454 assert!(parsed.passthrough);
455 assert!(!parsed.lazy);
456 assert!(parsed.source.is_empty());
457 }
458
459 #[test]
460 fn test_resolved_component_complex_source() {
461 let source = r#"Column {
462 Text("Header")
463 Row {
464 Button("Submit") { Text("OK") }
465 Button("Cancel") { Text("No") }
466 }
467}"#;
468 let component = ResolvedComponent {
469 source: source.to_string(),
470 path: "/components/Form.hypen".to_string(),
471 passthrough: false,
472 lazy: false,
473 };
474
475 let json = serde_json::to_string(&component).unwrap();
476 let parsed: ResolvedComponent = serde_json::from_str(&json).unwrap();
477
478 assert!(parsed.source.contains("Column"));
479 assert!(parsed.source.contains("Button"));
480 }
481
482 #[test]
487 fn test_action_payload_minimal() {
488 let json = r#"{"name": "click"}"#;
489 let action: ActionPayload = serde_json::from_str(json).unwrap();
490 assert_eq!(action.name, "click");
491 assert!(action.payload.is_null()); }
493
494 #[test]
495 fn test_action_payload_with_data() {
496 let action = ActionPayload {
497 name: "submit".to_string(),
498 payload: json!({"form": {"email": "test@example.com"}}),
499 };
500
501 let json = serde_json::to_string(&action).unwrap();
502 let parsed: ActionPayload = serde_json::from_str(&json).unwrap();
503 assert_eq!(parsed.name, "submit");
504 assert_eq!(parsed.payload["form"]["email"], "test@example.com");
505 }
506
507 #[test]
508 fn test_action_payload_with_array() {
509 let action = ActionPayload {
510 name: "selectItems".to_string(),
511 payload: json!({
512 "ids": [1, 2, 3, 4, 5],
513 "selectAll": true
514 }),
515 };
516
517 let json = serde_json::to_string(&action).unwrap();
518 let parsed: ActionPayload = serde_json::from_str(&json).unwrap();
519
520 assert_eq!(parsed.payload["ids"].as_array().unwrap().len(), 5);
521 assert_eq!(parsed.payload["selectAll"], true);
522 }
523
524 #[test]
525 fn test_action_payload_with_primitive_payload() {
526 let action = ActionPayload {
527 name: "setCount".to_string(),
528 payload: json!(42),
529 };
530
531 let json = serde_json::to_string(&action).unwrap();
532 let parsed: ActionPayload = serde_json::from_str(&json).unwrap();
533
534 assert_eq!(parsed.payload, 42);
535 }
536
537 #[test]
538 fn test_action_payload_roundtrip() {
539 let original = ActionPayload {
540 name: "complexAction".to_string(),
541 payload: json!({
542 "nested": {
543 "deeply": {
544 "value": "found"
545 }
546 }
547 }),
548 };
549
550 let json = serde_json::to_string(&original).unwrap();
551 let parsed: ActionPayload = serde_json::from_str(&json).unwrap();
552
553 assert_eq!(parsed.name, original.name);
554 assert_eq!(
555 parsed.payload["nested"]["deeply"]["value"],
556 original.payload["nested"]["deeply"]["value"]
557 );
558 }
559
560 #[test]
565 fn test_sparse_state_update() {
566 let update = SparseStateUpdate {
567 paths: vec!["user.name".to_string(), "count".to_string()],
568 values: json!({
569 "user.name": "Bob",
570 "count": 42
571 }),
572 };
573
574 let json = serde_json::to_string(&update).unwrap();
575 let parsed: SparseStateUpdate = serde_json::from_str(&json).unwrap();
576 assert_eq!(parsed.paths.len(), 2);
577 assert!(parsed.paths.contains(&"user.name".to_string()));
578 assert_eq!(parsed.values["count"], 42);
579 }
580
581 #[test]
582 fn test_sparse_state_update_empty() {
583 let update = SparseStateUpdate {
584 paths: vec![],
585 values: json!({}),
586 };
587
588 let json = serde_json::to_string(&update).unwrap();
589 assert!(json.contains("\"paths\":[]"));
590 }
591
592 #[test]
593 fn test_sparse_state_update_deeply_nested_paths() {
594 let update = SparseStateUpdate {
595 paths: vec![
596 "a.b.c.d".to_string(),
597 "x.y.z".to_string(),
598 ],
599 values: json!({
600 "a.b.c.d": "deep value",
601 "x.y.z": 123
602 }),
603 };
604
605 let json = serde_json::to_string(&update).unwrap();
606 let parsed: SparseStateUpdate = serde_json::from_str(&json).unwrap();
607
608 assert_eq!(parsed.paths.len(), 2);
609 assert_eq!(parsed.values["a.b.c.d"], "deep value");
610 assert_eq!(parsed.values["x.y.z"], 123);
611 }
612
613 #[test]
614 fn test_sparse_state_update_complex_values() {
615 let update = SparseStateUpdate {
616 paths: vec!["user".to_string(), "items".to_string()],
617 values: json!({
618 "user": {"id": 1, "name": "Alice"},
619 "items": [{"id": 1}, {"id": 2}]
620 }),
621 };
622
623 let json = serde_json::to_string(&update).unwrap();
624 let parsed: SparseStateUpdate = serde_json::from_str(&json).unwrap();
625
626 assert_eq!(parsed.values["user"]["id"], 1);
627 assert_eq!(parsed.values["items"].as_array().unwrap().len(), 2);
628 }
629
630 #[test]
636 fn test_json_bytes_roundtrip() {
637 let config = ModuleConfig {
639 name: "TestModule".to_string(),
640 actions: vec!["act1".to_string()],
641 state_keys: vec!["key1".to_string()],
642 initial_state: json!({"key1": "value"}),
643 };
644
645 let json_bytes = serde_json::to_vec(&config).unwrap();
647
648 let parsed: ModuleConfig = serde_json::from_slice(&json_bytes).unwrap();
650
651 assert_eq!(parsed.name, config.name);
652 assert_eq!(parsed.actions, config.actions);
653 }
654
655 #[test]
656 fn test_action_dispatch_pattern() {
657 let action = ActionPayload {
659 name: "submitForm".to_string(),
660 payload: json!({
661 "formData": {
662 "username": "alice",
663 "remember": true
664 }
665 }),
666 };
667
668 let json = serde_json::to_string(&action).unwrap();
670
671 let parsed: ActionPayload = serde_json::from_str(&json).unwrap();
673
674 assert_eq!(parsed.name, "submitForm");
675 assert_eq!(parsed.payload["formData"]["username"], "alice");
676 }
677
678 #[test]
679 fn test_state_update_pattern() {
680 let update_json = r#"{
682 "paths": ["counter", "user.lastActive"],
683 "values": {
684 "counter": 10,
685 "user.lastActive": "2024-01-01T00:00:00Z"
686 }
687 }"#;
688
689 let update: SparseStateUpdate = serde_json::from_str(update_json).unwrap();
690
691 assert!(update.paths.contains(&"counter".to_string()));
693 assert!(update.paths.contains(&"user.lastActive".to_string()));
694
695 assert_eq!(update.values["counter"], 10);
697 }
698
699 #[test]
700 fn test_error_result_pattern() {
701 let result: FfiResult<String> = FfiResult::Error {
703 message: "Parse error at line 5: unexpected token".to_string(),
704 };
705
706 let json = serde_json::to_string(&result).unwrap();
707
708 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
710 assert_eq!(parsed["status"], "error");
711 assert!(parsed["message"].as_str().unwrap().contains("line 5"));
712 }
713
714 #[test]
715 fn test_patches_array_pattern() {
716 use crate::reconcile::Patch;
718
719 let patches = vec![
720 Patch::Create {
721 id: "node1".to_string(),
722 element_type: "Text".to_string(),
723 props: indexmap::IndexMap::new(),
724 },
725 Patch::SetText {
726 id: "node1".to_string(),
727 text: "Hello World".to_string(),
728 },
729 ];
730
731 let json = serde_json::to_string(&patches).unwrap();
732 let parsed: Vec<Patch> = serde_json::from_str(&json).unwrap();
733
734 assert_eq!(parsed.len(), 2);
735 match &parsed[0] {
736 Patch::Create { element_type, .. } => assert_eq!(element_type, "Text"),
737 _ => panic!("Expected Create patch"),
738 }
739 }
740}