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