1use dampen_core::binding::UiBindable;
7use dampen_core::parser::error::ParseError;
8use dampen_core::state::AppState;
9use serde::{Serialize, de::DeserializeOwned};
10use std::collections::HashMap;
11use std::marker::PhantomData;
12use std::time::{Duration, Instant};
13
14#[derive(Clone)]
16struct ParsedDocumentCache {
17 document: dampen_core::ir::DampenDocument,
19
20 cached_at: Instant,
22}
23
24pub struct HotReloadContext<M> {
26 last_model_snapshot: Option<String>,
28
29 last_reload_timestamp: Instant,
31
32 reload_count: usize,
34
35 error: Option<String>,
37
38 parse_cache: HashMap<u64, ParsedDocumentCache>,
41
42 max_cache_size: usize,
44
45 _marker: PhantomData<M>,
46}
47
48impl<M: UiBindable> HotReloadContext<M> {
49 pub fn new() -> Self {
51 Self {
52 last_model_snapshot: None,
53 last_reload_timestamp: Instant::now(),
54 reload_count: 0,
55 error: None,
56 parse_cache: HashMap::new(),
57 max_cache_size: 10,
58 _marker: PhantomData,
59 }
60 }
61
62 pub fn with_cache_size(cache_size: usize) -> Self {
64 Self {
65 last_model_snapshot: None,
66 last_reload_timestamp: Instant::now(),
67 reload_count: 0,
68 error: None,
69 parse_cache: HashMap::new(),
70 max_cache_size: cache_size,
71 _marker: PhantomData,
72 }
73 }
74
75 fn get_cached_document(&self, xml_source: &str) -> Option<dampen_core::ir::DampenDocument> {
77 use std::collections::hash_map::DefaultHasher;
78 use std::hash::{Hash, Hasher};
79
80 let mut hasher = DefaultHasher::new();
81 xml_source.hash(&mut hasher);
82 let content_hash = hasher.finish();
83
84 self.parse_cache
85 .get(&content_hash)
86 .map(|entry| entry.document.clone())
87 }
88
89 fn cache_document(&mut self, xml_source: &str, document: dampen_core::ir::DampenDocument) {
91 use std::collections::hash_map::DefaultHasher;
92 use std::hash::{Hash, Hasher};
93
94 if self.parse_cache.len() >= self.max_cache_size {
96 if let Some(oldest_key) = self
97 .parse_cache
98 .iter()
99 .min_by_key(|(_, entry)| entry.cached_at)
100 .map(|(key, _)| *key)
101 {
102 self.parse_cache.remove(&oldest_key);
103 }
104 }
105
106 let mut hasher = DefaultHasher::new();
107 xml_source.hash(&mut hasher);
108 let content_hash = hasher.finish();
109
110 self.parse_cache.insert(
111 content_hash,
112 ParsedDocumentCache {
113 document,
114 cached_at: Instant::now(),
115 },
116 );
117 }
118
119 pub fn clear_cache(&mut self) {
121 self.parse_cache.clear();
122 }
123
124 pub fn cache_stats(&self) -> (usize, usize) {
126 (self.parse_cache.len(), self.max_cache_size)
127 }
128
129 pub fn performance_metrics(&self) -> ReloadPerformanceMetrics {
131 ReloadPerformanceMetrics {
132 reload_count: self.reload_count,
133 last_reload_latency: self.last_reload_latency(),
134 cache_hit_rate: self.calculate_cache_hit_rate(),
135 cache_size: self.parse_cache.len(),
136 }
137 }
138
139 fn calculate_cache_hit_rate(&self) -> f64 {
141 0.0
144 }
145
146 pub fn snapshot_model(&mut self, model: &M) -> Result<(), String>
148 where
149 M: Serialize,
150 {
151 match serde_json::to_string(model) {
152 Ok(json) => {
153 self.last_model_snapshot = Some(json);
154 Ok(())
155 }
156 Err(e) => Err(format!("Failed to serialize model: {}", e)),
157 }
158 }
159
160 pub fn restore_model(&self) -> Result<M, String>
162 where
163 M: DeserializeOwned,
164 {
165 match &self.last_model_snapshot {
166 Some(json) => serde_json::from_str(json)
167 .map_err(|e| format!("Failed to deserialize model: {}", e)),
168 None => Err("No model snapshot available".to_string()),
169 }
170 }
171
172 pub fn record_reload(&mut self, success: bool) {
174 self.reload_count += 1;
175 self.last_reload_timestamp = Instant::now();
176 if !success {
177 self.error = Some("Reload failed".to_string());
178 } else {
179 self.error = None;
180 }
181 }
182
183 pub fn record_reload_with_timing(&mut self, success: bool, elapsed: Duration) {
185 self.reload_count += 1;
186 self.last_reload_timestamp = Instant::now();
187 if !success {
188 self.error = Some("Reload failed".to_string());
189 } else {
190 self.error = None;
191 }
192
193 if success && elapsed.as_millis() > 300 {
195 eprintln!(
196 "Warning: Hot-reload took {}ms (target: <300ms)",
197 elapsed.as_millis()
198 );
199 }
200 }
201
202 pub fn last_reload_latency(&self) -> Duration {
204 self.last_reload_timestamp.elapsed()
205 }
206}
207
208impl<M: UiBindable> Default for HotReloadContext<M> {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214#[derive(Debug, Clone, Copy)]
216pub struct ReloadPerformanceMetrics {
217 pub reload_count: usize,
219
220 pub last_reload_latency: Duration,
222
223 pub cache_hit_rate: f64,
225
226 pub cache_size: usize,
228}
229
230impl ReloadPerformanceMetrics {
231 pub fn meets_target(&self) -> bool {
233 self.last_reload_latency.as_millis() < 300
234 }
235
236 pub fn latency_ms(&self) -> u128 {
238 self.last_reload_latency.as_millis()
239 }
240}
241
242#[derive(Debug)]
244pub enum ReloadResult<M: UiBindable> {
245 Success(AppState<M>),
247
248 ParseError(ParseError),
250
251 ValidationError(Vec<String>),
253
254 StateRestoreWarning(AppState<M>, String),
256}
257
258pub fn attempt_hot_reload<M, F>(
335 xml_source: &str,
336 current_state: &AppState<M>,
337 context: &mut HotReloadContext<M>,
338 create_handlers: F,
339) -> ReloadResult<M>
340where
341 M: UiBindable + Serialize + DeserializeOwned + Default,
342 F: FnOnce() -> dampen_core::handler::HandlerRegistry,
343{
344 let reload_start = Instant::now();
345
346 if let Err(e) = context.snapshot_model(¤t_state.model) {
348 eprintln!("Warning: Failed to snapshot model: {}", e);
350 }
351
352 let new_document = if let Some(cached_doc) = context.get_cached_document(xml_source) {
354 cached_doc
356 } else {
357 match dampen_core::parser::parse(xml_source) {
359 Ok(doc) => {
360 context.cache_document(xml_source, doc.clone());
361 doc
362 }
363 Err(err) => {
364 context.record_reload(false);
365 return ReloadResult::ParseError(err);
366 }
367 }
368 };
369
370 let new_handlers = create_handlers();
372
373 if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
375 context.record_reload(false);
376 let error_messages: Vec<String> = missing_handlers
377 .iter()
378 .map(|h| format!("Handler '{}' is referenced but not registered", h))
379 .collect();
380 return ReloadResult::ValidationError(error_messages);
381 }
382
383 let restored_model = match context.restore_model() {
385 Ok(model) => {
386 model
388 }
389 Err(e) => {
390 eprintln!("Warning: Failed to restore model ({}), using default", e);
392
393 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
395
396 context.record_reload(true);
397 return ReloadResult::StateRestoreWarning(new_state, e);
398 }
399 };
400
401 let new_state = AppState::with_all(new_document, restored_model, new_handlers);
403
404 let elapsed = reload_start.elapsed();
405 context.record_reload_with_timing(true, elapsed);
406 ReloadResult::Success(new_state)
407}
408
409pub async fn attempt_hot_reload_async<M, F>(
471 xml_source: String,
472 current_state: &AppState<M>,
473 context: &mut HotReloadContext<M>,
474 create_handlers: F,
475) -> ReloadResult<M>
476where
477 M: UiBindable + Serialize + DeserializeOwned + Default + Send + 'static,
478 F: FnOnce() -> dampen_core::handler::HandlerRegistry + Send + 'static,
479{
480 let reload_start = Instant::now();
481
482 if let Err(e) = context.snapshot_model(¤t_state.model) {
484 eprintln!("Warning: Failed to snapshot model: {}", e);
485 }
486
487 let model_snapshot = context.last_model_snapshot.clone();
489
490 let new_document = if let Some(cached_doc) = context.get_cached_document(&xml_source) {
492 cached_doc
494 } else {
495 let xml_for_parse = xml_source.clone();
497 let parse_result =
498 tokio::task::spawn_blocking(move || dampen_core::parser::parse(&xml_for_parse)).await;
499
500 match parse_result {
501 Ok(Ok(doc)) => {
502 context.cache_document(&xml_source, doc.clone());
503 doc
504 }
505 Ok(Err(err)) => {
506 context.record_reload(false);
507 return ReloadResult::ParseError(err);
508 }
509 Err(join_err) => {
510 context.record_reload(false);
511 let error = ParseError {
512 kind: dampen_core::parser::error::ParseErrorKind::XmlSyntax,
513 span: dampen_core::ir::span::Span::default(),
514 message: format!("Async parsing failed: {}", join_err),
515 suggestion: Some(
516 "Check if the XML file is accessible and not corrupted".to_string(),
517 ),
518 };
519 return ReloadResult::ParseError(error);
520 }
521 }
522 };
523
524 let new_handlers = create_handlers();
526
527 if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
529 context.record_reload(false);
530 let error_messages: Vec<String> = missing_handlers
531 .iter()
532 .map(|h| format!("Handler '{}' is referenced but not registered", h))
533 .collect();
534 return ReloadResult::ValidationError(error_messages);
535 }
536
537 let restored_model = match model_snapshot {
539 Some(json) => match serde_json::from_str::<M>(&json) {
540 Ok(model) => model,
541 Err(e) => {
542 eprintln!("Warning: Failed to restore model ({}), using default", e);
543 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
544 context.record_reload(true);
545 return ReloadResult::StateRestoreWarning(
546 new_state,
547 format!("Failed to deserialize model: {}", e),
548 );
549 }
550 },
551 None => {
552 eprintln!("Warning: No model snapshot available, using default");
553 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
554 context.record_reload(true);
555 return ReloadResult::StateRestoreWarning(
556 new_state,
557 "No model snapshot available".to_string(),
558 );
559 }
560 };
561
562 let new_state = AppState::with_all(new_document, restored_model, new_handlers);
564
565 let elapsed = reload_start.elapsed();
566 context.record_reload_with_timing(true, elapsed);
567 ReloadResult::Success(new_state)
568}
569
570fn collect_handler_names(document: &dampen_core::ir::DampenDocument) -> Vec<String> {
583 use std::collections::HashSet;
584
585 let mut handlers = HashSet::new();
586 collect_handlers_from_node(&document.root, &mut handlers);
587 handlers.into_iter().collect()
588}
589
590fn collect_handlers_from_node(
592 node: &dampen_core::ir::node::WidgetNode,
593 handlers: &mut std::collections::HashSet<String>,
594) {
595 for event in &node.events {
597 handlers.insert(event.handler.clone());
598 }
599
600 for child in &node.children {
602 collect_handlers_from_node(child, handlers);
603 }
604}
605
606fn validate_handlers(
617 document: &dampen_core::ir::DampenDocument,
618 registry: &dampen_core::handler::HandlerRegistry,
619) -> Result<(), Vec<String>> {
620 let referenced_handlers = collect_handler_names(document);
621 let mut missing_handlers = Vec::new();
622
623 for handler_name in referenced_handlers {
624 if registry.get(&handler_name).is_none() {
625 missing_handlers.push(handler_name);
626 }
627 }
628
629 if missing_handlers.is_empty() {
630 Ok(())
631 } else {
632 Err(missing_handlers)
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use serde::{Deserialize, Serialize};
640
641 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
642 struct TestModel {
643 count: i32,
644 name: String,
645 }
646
647 impl UiBindable for TestModel {
648 fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> {
649 None
650 }
651
652 fn available_fields() -> Vec<String> {
653 vec![]
654 }
655 }
656
657 impl Default for TestModel {
658 fn default() -> Self {
659 Self {
660 count: 0,
661 name: "default".to_string(),
662 }
663 }
664 }
665
666 #[test]
667 fn test_snapshot_model_success() {
668 let mut context = HotReloadContext::<TestModel>::new();
669 let model = TestModel {
670 count: 42,
671 name: "Alice".to_string(),
672 };
673
674 let result = context.snapshot_model(&model);
675 assert!(result.is_ok());
676 assert!(context.last_model_snapshot.is_some());
677 }
678
679 #[test]
680 fn test_restore_model_success() {
681 let mut context = HotReloadContext::<TestModel>::new();
682 let original = TestModel {
683 count: 42,
684 name: "Alice".to_string(),
685 };
686
687 context.snapshot_model(&original).unwrap();
689
690 let restored = context.restore_model().unwrap();
692 assert_eq!(restored, original);
693 }
694
695 #[test]
696 fn test_restore_model_no_snapshot() {
697 let context = HotReloadContext::<TestModel>::new();
698
699 let result = context.restore_model();
701 assert!(result.is_err());
702 assert!(result.unwrap_err().contains("No model snapshot"));
703 }
704
705 #[test]
706 fn test_snapshot_restore_round_trip() {
707 let mut context = HotReloadContext::<TestModel>::new();
708 let original = TestModel {
709 count: 999,
710 name: "Bob".to_string(),
711 };
712
713 context.snapshot_model(&original).unwrap();
715
716 let mut modified = original.clone();
717 modified.count = 0;
718 modified.name = "Changed".to_string();
719
720 let restored = context.restore_model().unwrap();
722 assert_eq!(restored, original);
723 assert_ne!(restored, modified);
724 }
725
726 #[test]
727 fn test_multiple_snapshots() {
728 let mut context = HotReloadContext::<TestModel>::new();
729
730 let model1 = TestModel {
732 count: 1,
733 name: "First".to_string(),
734 };
735 context.snapshot_model(&model1).unwrap();
736
737 let model2 = TestModel {
739 count: 2,
740 name: "Second".to_string(),
741 };
742 context.snapshot_model(&model2).unwrap();
743
744 let restored = context.restore_model().unwrap();
746 assert_eq!(restored, model2);
747 assert_ne!(restored, model1);
748 }
749
750 #[test]
751 fn test_record_reload() {
752 let mut context = HotReloadContext::<TestModel>::new();
753
754 assert_eq!(context.reload_count, 0);
755 assert!(context.error.is_none());
756
757 context.record_reload(true);
759 assert_eq!(context.reload_count, 1);
760 assert!(context.error.is_none());
761
762 context.record_reload(false);
764 assert_eq!(context.reload_count, 2);
765 assert!(context.error.is_some());
766
767 context.record_reload(true);
769 assert_eq!(context.reload_count, 3);
770 assert!(context.error.is_none());
771 }
772
773 #[test]
774 fn test_attempt_hot_reload_success() {
775 use dampen_core::handler::HandlerRegistry;
776 use dampen_core::parser;
777
778 let xml_v1 =
780 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
781 let doc_v1 = parser::parse(xml_v1).unwrap();
782 let model_v1 = TestModel {
783 count: 42,
784 name: "Alice".to_string(),
785 };
786 let registry_v1 = HandlerRegistry::new();
787 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
788
789 let mut context = HotReloadContext::<TestModel>::new();
791
792 let xml_v2 =
794 r#"<dampen version="1.0"><column><text value="Version 2" /></column></dampen>"#;
795
796 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
798
799 match result {
801 ReloadResult::Success(new_state) => {
802 assert_eq!(new_state.model.count, 42);
803 assert_eq!(new_state.model.name, "Alice");
804 assert_eq!(context.reload_count, 1);
805 }
806 _ => panic!("Expected Success, got {:?}", result),
807 }
808 }
809
810 #[test]
811 fn test_attempt_hot_reload_parse_error() {
812 use dampen_core::handler::HandlerRegistry;
813 use dampen_core::parser;
814
815 let xml_v1 =
817 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
818 let doc_v1 = parser::parse(xml_v1).unwrap();
819 let model_v1 = TestModel {
820 count: 10,
821 name: "Bob".to_string(),
822 };
823 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
824
825 let mut context = HotReloadContext::<TestModel>::new();
826
827 let xml_invalid = r#"<dampen version="1.0"><column><text value="Broken"#;
829
830 let result = attempt_hot_reload(xml_invalid, &state_v1, &mut context, || {
832 HandlerRegistry::new()
833 });
834
835 match result {
837 ReloadResult::ParseError(_err) => {
838 assert_eq!(context.reload_count, 1); }
841 _ => panic!("Expected ParseError, got {:?}", result),
842 }
843 }
844
845 #[test]
846 fn test_attempt_hot_reload_model_restore_failure() {
847 use dampen_core::handler::HandlerRegistry;
848 use dampen_core::parser;
849
850 let xml_v1 =
852 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
853 let doc_v1 = parser::parse(xml_v1).unwrap();
854 let model_v1 = TestModel {
855 count: 99,
856 name: "Charlie".to_string(),
857 };
858 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
859
860 let mut context = HotReloadContext::<TestModel>::new();
862 context.last_model_snapshot = Some("{ invalid json }".to_string()); let xml_v2 =
866 r#"<dampen version="1.0"><column><text value="Version 2" /></column></dampen>"#;
867
868 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
870
871 match result {
878 ReloadResult::Success(new_state) => {
879 assert_eq!(new_state.model.count, 99);
881 assert_eq!(new_state.model.name, "Charlie");
882 assert_eq!(context.reload_count, 1);
883 }
884 _ => panic!("Expected Success, got {:?}", result),
885 }
886 }
887
888 #[test]
889 fn test_attempt_hot_reload_preserves_model_across_multiple_reloads() {
890 use dampen_core::handler::HandlerRegistry;
891 use dampen_core::parser;
892
893 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
895 let doc_v1 = parser::parse(xml_v1).unwrap();
896 let model_v1 = TestModel {
897 count: 100,
898 name: "Dave".to_string(),
899 };
900 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
901
902 let mut context = HotReloadContext::<TestModel>::new();
903
904 let xml_v2 = r#"<dampen version="1.0"><column><text value="V2" /></column></dampen>"#;
906 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
907
908 let state_v2 = match result {
909 ReloadResult::Success(s) => s,
910 _ => panic!("First reload failed"),
911 };
912
913 assert_eq!(state_v2.model.count, 100);
914 assert_eq!(state_v2.model.name, "Dave");
915
916 let xml_v3 = r#"<dampen version="1.0"><column><text value="V3" /></column></dampen>"#;
918 let result = attempt_hot_reload(xml_v3, &state_v2, &mut context, || HandlerRegistry::new());
919
920 let state_v3 = match result {
921 ReloadResult::Success(s) => s,
922 _ => panic!("Second reload failed"),
923 };
924
925 assert_eq!(state_v3.model.count, 100);
927 assert_eq!(state_v3.model.name, "Dave");
928 assert_eq!(context.reload_count, 2);
929 }
930
931 #[test]
932 fn test_attempt_hot_reload_with_handler_registry() {
933 use dampen_core::handler::HandlerRegistry;
934 use dampen_core::parser;
935
936 let xml_v1 = r#"<dampen version="1.0"><column><button label="Click" on_click="test" /></column></dampen>"#;
938 let doc_v1 = parser::parse(xml_v1).unwrap();
939 let model_v1 = TestModel {
940 count: 5,
941 name: "Eve".to_string(),
942 };
943
944 let registry_v1 = HandlerRegistry::new();
945 registry_v1.register_simple("test", |_model| {
946 });
948
949 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
950
951 let mut context = HotReloadContext::<TestModel>::new();
952
953 let xml_v2 = r#"<dampen version="1.0"><column><button label="Click Me" on_click="test2" /></column></dampen>"#;
955
956 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
958 let registry = HandlerRegistry::new();
959 registry.register_simple("test2", |_model| {
960 });
962 registry
963 });
964
965 match result {
967 ReloadResult::Success(new_state) => {
968 assert_eq!(new_state.model.count, 5);
970 assert_eq!(new_state.model.name, "Eve");
971
972 assert!(new_state.handler_registry.get("test2").is_some());
974 }
975 _ => panic!("Expected Success, got {:?}", result),
976 }
977 }
978
979 #[test]
980 fn test_collect_handler_names() {
981 use dampen_core::parser;
982
983 let xml = r#"
985 <dampen version="1.0">
986 <column>
987 <button label="Click" on_click="handle_click" />
988 <text_input placeholder="Type" on_input="handle_input" />
989 <button label="Submit" on_click="handle_submit" />
990 </column>
991 </dampen>
992 "#;
993
994 let doc = parser::parse(xml).unwrap();
995 let handlers = collect_handler_names(&doc);
996
997 assert_eq!(handlers.len(), 3);
999 assert!(handlers.contains(&"handle_click".to_string()));
1000 assert!(handlers.contains(&"handle_input".to_string()));
1001 assert!(handlers.contains(&"handle_submit".to_string()));
1002 }
1003
1004 #[test]
1005 fn test_collect_handler_names_nested() {
1006 use dampen_core::parser;
1007
1008 let xml = r#"
1010 <dampen version="1.0">
1011 <column>
1012 <row>
1013 <button label="A" on_click="handler_a" />
1014 </row>
1015 <row>
1016 <button label="B" on_click="handler_b" />
1017 <column>
1018 <button label="C" on_click="handler_c" />
1019 </column>
1020 </row>
1021 </column>
1022 </dampen>
1023 "#;
1024
1025 let doc = parser::parse(xml).unwrap();
1026 let handlers = collect_handler_names(&doc);
1027
1028 assert_eq!(handlers.len(), 3);
1030 assert!(handlers.contains(&"handler_a".to_string()));
1031 assert!(handlers.contains(&"handler_b".to_string()));
1032 assert!(handlers.contains(&"handler_c".to_string()));
1033 }
1034
1035 #[test]
1036 fn test_collect_handler_names_duplicates() {
1037 use dampen_core::parser;
1038
1039 let xml = r#"
1041 <dampen version="1.0">
1042 <column>
1043 <button label="1" on_click="same_handler" />
1044 <button label="2" on_click="same_handler" />
1045 <button label="3" on_click="same_handler" />
1046 </column>
1047 </dampen>
1048 "#;
1049
1050 let doc = parser::parse(xml).unwrap();
1051 let handlers = collect_handler_names(&doc);
1052
1053 assert_eq!(handlers.len(), 1);
1055 assert!(handlers.contains(&"same_handler".to_string()));
1056 }
1057
1058 #[test]
1059 fn test_validate_handlers_all_present() {
1060 use dampen_core::handler::HandlerRegistry;
1061 use dampen_core::parser;
1062
1063 let xml = r#"
1064 <dampen version="1.0">
1065 <column>
1066 <button label="Click" on_click="test_handler" />
1067 </column>
1068 </dampen>
1069 "#;
1070
1071 let doc = parser::parse(xml).unwrap();
1072 let registry = HandlerRegistry::new();
1073 registry.register_simple("test_handler", |_model| {});
1074
1075 let result = validate_handlers(&doc, ®istry);
1076 assert!(result.is_ok());
1077 }
1078
1079 #[test]
1080 fn test_validate_handlers_missing() {
1081 use dampen_core::handler::HandlerRegistry;
1082 use dampen_core::parser;
1083
1084 let xml = r#"
1085 <dampen version="1.0">
1086 <column>
1087 <button label="Click" on_click="missing_handler" />
1088 </column>
1089 </dampen>
1090 "#;
1091
1092 let doc = parser::parse(xml).unwrap();
1093 let registry = HandlerRegistry::new();
1094 let result = validate_handlers(&doc, ®istry);
1097 assert!(result.is_err());
1098
1099 let missing = result.unwrap_err();
1100 assert_eq!(missing.len(), 1);
1101 assert_eq!(missing[0], "missing_handler");
1102 }
1103
1104 #[test]
1105 fn test_validate_handlers_multiple_missing() {
1106 use dampen_core::handler::HandlerRegistry;
1107 use dampen_core::parser;
1108
1109 let xml = r#"
1110 <dampen version="1.0">
1111 <column>
1112 <button label="A" on_click="handler_a" />
1113 <button label="B" on_click="handler_b" />
1114 <button label="C" on_click="handler_c" />
1115 </column>
1116 </dampen>
1117 "#;
1118
1119 let doc = parser::parse(xml).unwrap();
1120 let registry = HandlerRegistry::new();
1121 registry.register_simple("handler_b", |_model| {});
1123
1124 let result = validate_handlers(&doc, ®istry);
1125 assert!(result.is_err());
1126
1127 let missing = result.unwrap_err();
1128 assert_eq!(missing.len(), 2);
1129 assert!(missing.contains(&"handler_a".to_string()));
1130 assert!(missing.contains(&"handler_c".to_string()));
1131 }
1132
1133 #[test]
1134 fn test_attempt_hot_reload_validation_error() {
1135 use dampen_core::handler::HandlerRegistry;
1136 use dampen_core::parser;
1137
1138 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1140 let doc_v1 = parser::parse(xml_v1).unwrap();
1141 let model_v1 = TestModel {
1142 count: 10,
1143 name: "Test".to_string(),
1144 };
1145 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1146
1147 let mut context = HotReloadContext::<TestModel>::new();
1148
1149 let xml_v2 = r#"
1151 <dampen version="1.0">
1152 <column>
1153 <button label="Click" on_click="unregistered_handler" />
1154 </column>
1155 </dampen>
1156 "#;
1157
1158 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1160 HandlerRegistry::new() });
1162
1163 match result {
1165 ReloadResult::ValidationError(errors) => {
1166 assert!(!errors.is_empty());
1167 assert!(errors[0].contains("unregistered_handler"));
1168 assert_eq!(context.reload_count, 1); }
1170 _ => panic!("Expected ValidationError, got {:?}", result),
1171 }
1172 }
1173
1174 #[test]
1175 fn test_attempt_hot_reload_validation_success() {
1176 use dampen_core::handler::HandlerRegistry;
1177 use dampen_core::parser;
1178
1179 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1181 let doc_v1 = parser::parse(xml_v1).unwrap();
1182 let model_v1 = TestModel {
1183 count: 20,
1184 name: "Valid".to_string(),
1185 };
1186 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1187
1188 let mut context = HotReloadContext::<TestModel>::new();
1189
1190 let xml_v2 = r#"
1192 <dampen version="1.0">
1193 <column>
1194 <button label="Click" on_click="registered_handler" />
1195 </column>
1196 </dampen>
1197 "#;
1198
1199 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1201 let registry = HandlerRegistry::new();
1202 registry.register_simple("registered_handler", |_model| {});
1203 registry
1204 });
1205
1206 match result {
1208 ReloadResult::Success(new_state) => {
1209 assert_eq!(new_state.model.count, 20);
1210 assert_eq!(new_state.model.name, "Valid");
1211 assert_eq!(context.reload_count, 1);
1212 }
1213 _ => panic!("Expected Success, got {:?}", result),
1214 }
1215 }
1216
1217 #[test]
1218 fn test_handler_registry_complete_replacement() {
1219 use dampen_core::handler::HandlerRegistry;
1220 use dampen_core::parser;
1221
1222 let xml_v1 = r#"
1224 <dampen version="1.0">
1225 <column>
1226 <button label="Old" on_click="old_handler" />
1227 </column>
1228 </dampen>
1229 "#;
1230 let doc_v1 = parser::parse(xml_v1).unwrap();
1231 let model_v1 = TestModel {
1232 count: 1,
1233 name: "Initial".to_string(),
1234 };
1235
1236 let registry_v1 = HandlerRegistry::new();
1237 registry_v1.register_simple("old_handler", |_model| {});
1238
1239 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1240
1241 assert!(state_v1.handler_registry.get("old_handler").is_some());
1243
1244 let mut context = HotReloadContext::<TestModel>::new();
1245
1246 let xml_v2 = r#"
1248 <dampen version="1.0">
1249 <column>
1250 <button label="New" on_click="new_handler" />
1251 <button label="Another" on_click="another_handler" />
1252 </column>
1253 </dampen>
1254 "#;
1255
1256 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1258 let registry = HandlerRegistry::new();
1259 registry.register_simple("new_handler", |_model| {});
1260 registry.register_simple("another_handler", |_model| {});
1261 registry
1262 });
1263
1264 match result {
1266 ReloadResult::Success(new_state) => {
1267 assert_eq!(new_state.model.count, 1);
1269 assert_eq!(new_state.model.name, "Initial");
1270
1271 assert!(new_state.handler_registry.get("old_handler").is_none());
1273
1274 assert!(new_state.handler_registry.get("new_handler").is_some());
1276 assert!(new_state.handler_registry.get("another_handler").is_some());
1277 }
1278 _ => panic!("Expected Success, got {:?}", result),
1279 }
1280 }
1281
1282 #[test]
1283 fn test_handler_registry_rebuild_before_validation() {
1284 use dampen_core::handler::HandlerRegistry;
1285 use dampen_core::parser;
1286
1287 let xml_v1 = r#"<dampen version="1.0"><column><button on_click="handler_a" label="A" /></column></dampen>"#;
1292 let doc_v1 = parser::parse(xml_v1).unwrap();
1293 let model_v1 = TestModel {
1294 count: 100,
1295 name: "Test".to_string(),
1296 };
1297
1298 let registry_v1 = HandlerRegistry::new();
1299 registry_v1.register_simple("handler_a", |_model| {});
1300
1301 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1302
1303 let mut context = HotReloadContext::<TestModel>::new();
1304
1305 let xml_v2 = r#"<dampen version="1.0"><column><button on_click="handler_b" label="B" /></column></dampen>"#;
1307
1308 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1310 let registry = HandlerRegistry::new();
1311 registry.register_simple("handler_b", |_model| {}); registry
1313 });
1314
1315 match result {
1317 ReloadResult::Success(new_state) => {
1318 assert_eq!(new_state.model.count, 100);
1319 assert!(new_state.handler_registry.get("handler_b").is_some());
1321 assert!(new_state.handler_registry.get("handler_a").is_none());
1323 }
1324 _ => panic!(
1325 "Expected Success (registry rebuilt before validation), got {:?}",
1326 result
1327 ),
1328 }
1329 }
1330}