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