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