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 = r#"<dampen><column><text value="Version 1" /></column></dampen>"#;
780 let doc_v1 = parser::parse(xml_v1).unwrap();
781 let model_v1 = TestModel {
782 count: 42,
783 name: "Alice".to_string(),
784 };
785 let registry_v1 = HandlerRegistry::new();
786 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
787
788 let mut context = HotReloadContext::<TestModel>::new();
790
791 let xml_v2 = r#"<dampen><column><text value="Version 2" /></column></dampen>"#;
793
794 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
796
797 match result {
799 ReloadResult::Success(new_state) => {
800 assert_eq!(new_state.model.count, 42);
801 assert_eq!(new_state.model.name, "Alice");
802 assert_eq!(context.reload_count, 1);
803 }
804 _ => panic!("Expected Success, got {:?}", result),
805 }
806 }
807
808 #[test]
809 fn test_attempt_hot_reload_parse_error() {
810 use dampen_core::handler::HandlerRegistry;
811 use dampen_core::parser;
812
813 let xml_v1 = r#"<dampen><column><text value="Version 1" /></column></dampen>"#;
815 let doc_v1 = parser::parse(xml_v1).unwrap();
816 let model_v1 = TestModel {
817 count: 10,
818 name: "Bob".to_string(),
819 };
820 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
821
822 let mut context = HotReloadContext::<TestModel>::new();
823
824 let xml_invalid = r#"<dampen><column><text value="Broken"#;
826
827 let result = attempt_hot_reload(xml_invalid, &state_v1, &mut context, || {
829 HandlerRegistry::new()
830 });
831
832 match result {
834 ReloadResult::ParseError(_err) => {
835 assert_eq!(context.reload_count, 1); }
838 _ => panic!("Expected ParseError, got {:?}", result),
839 }
840 }
841
842 #[test]
843 fn test_attempt_hot_reload_model_restore_failure() {
844 use dampen_core::handler::HandlerRegistry;
845 use dampen_core::parser;
846
847 let xml_v1 = r#"<dampen><column><text value="Version 1" /></column></dampen>"#;
849 let doc_v1 = parser::parse(xml_v1).unwrap();
850 let model_v1 = TestModel {
851 count: 99,
852 name: "Charlie".to_string(),
853 };
854 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
855
856 let mut context = HotReloadContext::<TestModel>::new();
858 context.last_model_snapshot = Some("{ invalid json }".to_string()); let xml_v2 = r#"<dampen><column><text value="Version 2" /></column></dampen>"#;
862
863 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
865
866 match result {
873 ReloadResult::Success(new_state) => {
874 assert_eq!(new_state.model.count, 99);
876 assert_eq!(new_state.model.name, "Charlie");
877 assert_eq!(context.reload_count, 1);
878 }
879 _ => panic!("Expected Success, got {:?}", result),
880 }
881 }
882
883 #[test]
884 fn test_attempt_hot_reload_preserves_model_across_multiple_reloads() {
885 use dampen_core::handler::HandlerRegistry;
886 use dampen_core::parser;
887
888 let xml_v1 = r#"<dampen><column><text value="V1" /></column></dampen>"#;
890 let doc_v1 = parser::parse(xml_v1).unwrap();
891 let model_v1 = TestModel {
892 count: 100,
893 name: "Dave".to_string(),
894 };
895 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
896
897 let mut context = HotReloadContext::<TestModel>::new();
898
899 let xml_v2 = r#"<dampen><column><text value="V2" /></column></dampen>"#;
901 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
902
903 let state_v2 = match result {
904 ReloadResult::Success(s) => s,
905 _ => panic!("First reload failed"),
906 };
907
908 assert_eq!(state_v2.model.count, 100);
909 assert_eq!(state_v2.model.name, "Dave");
910
911 let xml_v3 = r#"<dampen><column><text value="V3" /></column></dampen>"#;
913 let result = attempt_hot_reload(xml_v3, &state_v2, &mut context, || HandlerRegistry::new());
914
915 let state_v3 = match result {
916 ReloadResult::Success(s) => s,
917 _ => panic!("Second reload failed"),
918 };
919
920 assert_eq!(state_v3.model.count, 100);
922 assert_eq!(state_v3.model.name, "Dave");
923 assert_eq!(context.reload_count, 2);
924 }
925
926 #[test]
927 fn test_attempt_hot_reload_with_handler_registry() {
928 use dampen_core::handler::HandlerRegistry;
929 use dampen_core::parser;
930
931 let xml_v1 =
933 r#"<dampen><column><button label="Click" on_click="test" /></column></dampen>"#;
934 let doc_v1 = parser::parse(xml_v1).unwrap();
935 let model_v1 = TestModel {
936 count: 5,
937 name: "Eve".to_string(),
938 };
939
940 let registry_v1 = HandlerRegistry::new();
941 registry_v1.register_simple("test", |_model| {
942 });
944
945 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
946
947 let mut context = HotReloadContext::<TestModel>::new();
948
949 let xml_v2 =
951 r#"<dampen><column><button label="Click Me" on_click="test2" /></column></dampen>"#;
952
953 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
955 let registry = HandlerRegistry::new();
956 registry.register_simple("test2", |_model| {
957 });
959 registry
960 });
961
962 match result {
964 ReloadResult::Success(new_state) => {
965 assert_eq!(new_state.model.count, 5);
967 assert_eq!(new_state.model.name, "Eve");
968
969 assert!(new_state.handler_registry.get("test2").is_some());
971 }
972 _ => panic!("Expected Success, got {:?}", result),
973 }
974 }
975
976 #[test]
977 fn test_collect_handler_names() {
978 use dampen_core::parser;
979
980 let xml = r#"
982 <dampen>
983 <column>
984 <button label="Click" on_click="handle_click" />
985 <text_input placeholder="Type" on_input="handle_input" />
986 <button label="Submit" on_click="handle_submit" />
987 </column>
988 </dampen>
989 "#;
990
991 let doc = parser::parse(xml).unwrap();
992 let handlers = collect_handler_names(&doc);
993
994 assert_eq!(handlers.len(), 3);
996 assert!(handlers.contains(&"handle_click".to_string()));
997 assert!(handlers.contains(&"handle_input".to_string()));
998 assert!(handlers.contains(&"handle_submit".to_string()));
999 }
1000
1001 #[test]
1002 fn test_collect_handler_names_nested() {
1003 use dampen_core::parser;
1004
1005 let xml = r#"
1007 <dampen>
1008 <column>
1009 <row>
1010 <button label="A" on_click="handler_a" />
1011 </row>
1012 <row>
1013 <button label="B" on_click="handler_b" />
1014 <column>
1015 <button label="C" on_click="handler_c" />
1016 </column>
1017 </row>
1018 </column>
1019 </dampen>
1020 "#;
1021
1022 let doc = parser::parse(xml).unwrap();
1023 let handlers = collect_handler_names(&doc);
1024
1025 assert_eq!(handlers.len(), 3);
1027 assert!(handlers.contains(&"handler_a".to_string()));
1028 assert!(handlers.contains(&"handler_b".to_string()));
1029 assert!(handlers.contains(&"handler_c".to_string()));
1030 }
1031
1032 #[test]
1033 fn test_collect_handler_names_duplicates() {
1034 use dampen_core::parser;
1035
1036 let xml = r#"
1038 <dampen>
1039 <column>
1040 <button label="1" on_click="same_handler" />
1041 <button label="2" on_click="same_handler" />
1042 <button label="3" on_click="same_handler" />
1043 </column>
1044 </dampen>
1045 "#;
1046
1047 let doc = parser::parse(xml).unwrap();
1048 let handlers = collect_handler_names(&doc);
1049
1050 assert_eq!(handlers.len(), 1);
1052 assert!(handlers.contains(&"same_handler".to_string()));
1053 }
1054
1055 #[test]
1056 fn test_validate_handlers_all_present() {
1057 use dampen_core::handler::HandlerRegistry;
1058 use dampen_core::parser;
1059
1060 let xml = r#"
1061 <dampen>
1062 <column>
1063 <button label="Click" on_click="test_handler" />
1064 </column>
1065 </dampen>
1066 "#;
1067
1068 let doc = parser::parse(xml).unwrap();
1069 let registry = HandlerRegistry::new();
1070 registry.register_simple("test_handler", |_model| {});
1071
1072 let result = validate_handlers(&doc, ®istry);
1073 assert!(result.is_ok());
1074 }
1075
1076 #[test]
1077 fn test_validate_handlers_missing() {
1078 use dampen_core::handler::HandlerRegistry;
1079 use dampen_core::parser;
1080
1081 let xml = r#"
1082 <dampen>
1083 <column>
1084 <button label="Click" on_click="missing_handler" />
1085 </column>
1086 </dampen>
1087 "#;
1088
1089 let doc = parser::parse(xml).unwrap();
1090 let registry = HandlerRegistry::new();
1091 let result = validate_handlers(&doc, ®istry);
1094 assert!(result.is_err());
1095
1096 let missing = result.unwrap_err();
1097 assert_eq!(missing.len(), 1);
1098 assert_eq!(missing[0], "missing_handler");
1099 }
1100
1101 #[test]
1102 fn test_validate_handlers_multiple_missing() {
1103 use dampen_core::handler::HandlerRegistry;
1104 use dampen_core::parser;
1105
1106 let xml = r#"
1107 <dampen>
1108 <column>
1109 <button label="A" on_click="handler_a" />
1110 <button label="B" on_click="handler_b" />
1111 <button label="C" on_click="handler_c" />
1112 </column>
1113 </dampen>
1114 "#;
1115
1116 let doc = parser::parse(xml).unwrap();
1117 let registry = HandlerRegistry::new();
1118 registry.register_simple("handler_b", |_model| {});
1120
1121 let result = validate_handlers(&doc, ®istry);
1122 assert!(result.is_err());
1123
1124 let missing = result.unwrap_err();
1125 assert_eq!(missing.len(), 2);
1126 assert!(missing.contains(&"handler_a".to_string()));
1127 assert!(missing.contains(&"handler_c".to_string()));
1128 }
1129
1130 #[test]
1131 fn test_attempt_hot_reload_validation_error() {
1132 use dampen_core::handler::HandlerRegistry;
1133 use dampen_core::parser;
1134
1135 let xml_v1 = r#"<dampen><column><text value="V1" /></column></dampen>"#;
1137 let doc_v1 = parser::parse(xml_v1).unwrap();
1138 let model_v1 = TestModel {
1139 count: 10,
1140 name: "Test".to_string(),
1141 };
1142 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1143
1144 let mut context = HotReloadContext::<TestModel>::new();
1145
1146 let xml_v2 = r#"
1148 <dampen>
1149 <column>
1150 <button label="Click" on_click="unregistered_handler" />
1151 </column>
1152 </dampen>
1153 "#;
1154
1155 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1157 HandlerRegistry::new() });
1159
1160 match result {
1162 ReloadResult::ValidationError(errors) => {
1163 assert!(!errors.is_empty());
1164 assert!(errors[0].contains("unregistered_handler"));
1165 assert_eq!(context.reload_count, 1); }
1167 _ => panic!("Expected ValidationError, got {:?}", result),
1168 }
1169 }
1170
1171 #[test]
1172 fn test_attempt_hot_reload_validation_success() {
1173 use dampen_core::handler::HandlerRegistry;
1174 use dampen_core::parser;
1175
1176 let xml_v1 = r#"<dampen><column><text value="V1" /></column></dampen>"#;
1178 let doc_v1 = parser::parse(xml_v1).unwrap();
1179 let model_v1 = TestModel {
1180 count: 20,
1181 name: "Valid".to_string(),
1182 };
1183 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1184
1185 let mut context = HotReloadContext::<TestModel>::new();
1186
1187 let xml_v2 = r#"
1189 <dampen>
1190 <column>
1191 <button label="Click" on_click="registered_handler" />
1192 </column>
1193 </dampen>
1194 "#;
1195
1196 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1198 let registry = HandlerRegistry::new();
1199 registry.register_simple("registered_handler", |_model| {});
1200 registry
1201 });
1202
1203 match result {
1205 ReloadResult::Success(new_state) => {
1206 assert_eq!(new_state.model.count, 20);
1207 assert_eq!(new_state.model.name, "Valid");
1208 assert_eq!(context.reload_count, 1);
1209 }
1210 _ => panic!("Expected Success, got {:?}", result),
1211 }
1212 }
1213
1214 #[test]
1215 fn test_handler_registry_complete_replacement() {
1216 use dampen_core::handler::HandlerRegistry;
1217 use dampen_core::parser;
1218
1219 let xml_v1 = r#"
1221 <dampen>
1222 <column>
1223 <button label="Old" on_click="old_handler" />
1224 </column>
1225 </dampen>
1226 "#;
1227 let doc_v1 = parser::parse(xml_v1).unwrap();
1228 let model_v1 = TestModel {
1229 count: 1,
1230 name: "Initial".to_string(),
1231 };
1232
1233 let registry_v1 = HandlerRegistry::new();
1234 registry_v1.register_simple("old_handler", |_model| {});
1235
1236 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1237
1238 assert!(state_v1.handler_registry.get("old_handler").is_some());
1240
1241 let mut context = HotReloadContext::<TestModel>::new();
1242
1243 let xml_v2 = r#"
1245 <dampen>
1246 <column>
1247 <button label="New" on_click="new_handler" />
1248 <button label="Another" on_click="another_handler" />
1249 </column>
1250 </dampen>
1251 "#;
1252
1253 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1255 let registry = HandlerRegistry::new();
1256 registry.register_simple("new_handler", |_model| {});
1257 registry.register_simple("another_handler", |_model| {});
1258 registry
1259 });
1260
1261 match result {
1263 ReloadResult::Success(new_state) => {
1264 assert_eq!(new_state.model.count, 1);
1266 assert_eq!(new_state.model.name, "Initial");
1267
1268 assert!(new_state.handler_registry.get("old_handler").is_none());
1270
1271 assert!(new_state.handler_registry.get("new_handler").is_some());
1273 assert!(new_state.handler_registry.get("another_handler").is_some());
1274 }
1275 _ => panic!("Expected Success, got {:?}", result),
1276 }
1277 }
1278
1279 #[test]
1280 fn test_handler_registry_rebuild_before_validation() {
1281 use dampen_core::handler::HandlerRegistry;
1282 use dampen_core::parser;
1283
1284 let xml_v1 =
1289 r#"<dampen><column><button on_click="handler_a" label="A" /></column></dampen>"#;
1290 let doc_v1 = parser::parse(xml_v1).unwrap();
1291 let model_v1 = TestModel {
1292 count: 100,
1293 name: "Test".to_string(),
1294 };
1295
1296 let registry_v1 = HandlerRegistry::new();
1297 registry_v1.register_simple("handler_a", |_model| {});
1298
1299 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1300
1301 let mut context = HotReloadContext::<TestModel>::new();
1302
1303 let xml_v2 =
1305 r#"<dampen><column><button on_click="handler_b" label="B" /></column></dampen>"#;
1306
1307 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1309 let registry = HandlerRegistry::new();
1310 registry.register_simple("handler_b", |_model| {}); registry
1312 });
1313
1314 match result {
1316 ReloadResult::Success(new_state) => {
1317 assert_eq!(new_state.model.count, 100);
1318 assert!(new_state.handler_registry.get("handler_b").is_some());
1320 assert!(new_state.handler_registry.get("handler_a").is_none());
1322 }
1323 _ => panic!(
1324 "Expected Success (registry rebuilt before validation), got {:?}",
1325 result
1326 ),
1327 }
1328 }
1329}