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