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::sync::Arc;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::time::{Duration, Instant};
15
16#[derive(Clone)]
18struct ParsedDocumentCache {
19 document: dampen_core::ir::DampenDocument,
21
22 cached_at: Instant,
24}
25
26pub struct HotReloadContext<M> {
28 last_model_snapshot: Option<String>,
30
31 last_reload_timestamp: Instant,
33
34 reload_count: usize,
36
37 error: Option<String>,
39
40 parse_cache: HashMap<u64, ParsedDocumentCache>,
43
44 max_cache_size: usize,
46
47 cache_hits: AtomicUsize,
49
50 cache_misses: AtomicUsize,
52
53 _marker: PhantomData<M>,
54}
55
56impl<M: UiBindable> HotReloadContext<M> {
57 pub fn new() -> Self {
59 Self {
60 last_model_snapshot: None,
61 last_reload_timestamp: Instant::now(),
62 reload_count: 0,
63 error: None,
64 parse_cache: HashMap::new(),
65 max_cache_size: 10,
66 cache_hits: AtomicUsize::new(0),
67 cache_misses: AtomicUsize::new(0),
68 _marker: PhantomData,
69 }
70 }
71
72 pub fn with_cache_size(cache_size: usize) -> Self {
74 Self {
75 last_model_snapshot: None,
76 last_reload_timestamp: Instant::now(),
77 reload_count: 0,
78 error: None,
79 parse_cache: HashMap::new(),
80 max_cache_size: cache_size,
81 cache_hits: AtomicUsize::new(0),
82 cache_misses: AtomicUsize::new(0),
83 _marker: PhantomData,
84 }
85 }
86
87 fn compute_content_hash(xml_source: &str) -> u64 {
89 use std::collections::hash_map::DefaultHasher;
90 use std::hash::{Hash, Hasher};
91
92 let mut hasher = DefaultHasher::new();
93 xml_source.hash(&mut hasher);
94 hasher.finish()
95 }
96
97 fn get_cached_document(&self, xml_source: &str) -> Option<dampen_core::ir::DampenDocument> {
99 let content_hash = Self::compute_content_hash(xml_source);
100
101 self.parse_cache
102 .get(&content_hash)
103 .map(|entry| entry.document.clone())
104 .inspect(|_| {
105 self.cache_hits.fetch_add(1, Ordering::Relaxed);
106 })
107 .or_else(|| {
108 self.cache_misses.fetch_add(1, Ordering::Relaxed);
109 None
110 })
111 }
112
113 fn cache_document(&mut self, xml_source: &str, document: dampen_core::ir::DampenDocument) {
115 if self.parse_cache.len() >= self.max_cache_size
117 && let Some(oldest_key) = self
118 .parse_cache
119 .iter()
120 .min_by_key(|(_, entry)| entry.cached_at)
121 .map(|(key, _)| *key)
122 {
123 self.parse_cache.remove(&oldest_key);
124 }
125
126 let content_hash = Self::compute_content_hash(xml_source);
127
128 self.parse_cache.insert(
129 content_hash,
130 ParsedDocumentCache {
131 document,
132 cached_at: Instant::now(),
133 },
134 );
135 }
136
137 pub fn clear_cache(&mut self) {
139 self.parse_cache.clear();
140 }
141
142 pub fn cache_stats(&self) -> (usize, usize) {
144 (self.parse_cache.len(), self.max_cache_size)
145 }
146
147 pub fn performance_metrics(&self) -> ReloadPerformanceMetrics {
149 ReloadPerformanceMetrics {
150 reload_count: self.reload_count,
151 last_reload_latency: self.last_reload_latency(),
152 cache_hit_rate: self.calculate_cache_hit_rate(),
153 cache_size: self.parse_cache.len(),
154 }
155 }
156
157 fn calculate_cache_hit_rate(&self) -> f64 {
159 let hits = self.cache_hits.load(Ordering::Relaxed);
160 let misses = self.cache_misses.load(Ordering::Relaxed);
161 let total = hits.saturating_add(misses);
162
163 if total == 0 {
164 0.0
165 } else {
166 hits as f64 / total as f64
167 }
168 }
169
170 pub fn snapshot_model(&mut self, model: &M) -> Result<(), String>
172 where
173 M: Serialize,
174 {
175 match serde_json::to_string(model) {
176 Ok(json) => {
177 self.last_model_snapshot = Some(json);
178 Ok(())
179 }
180 Err(e) => Err(format!("Failed to serialize model: {}", e)),
181 }
182 }
183
184 pub fn restore_model(&self) -> Result<M, String>
186 where
187 M: DeserializeOwned,
188 {
189 match &self.last_model_snapshot {
190 Some(json) => serde_json::from_str(json)
191 .map_err(|e| format!("Failed to deserialize model: {}", e)),
192 None => Err("No model snapshot available".to_string()),
193 }
194 }
195
196 pub fn record_reload(&mut self, success: bool) {
198 self.reload_count += 1;
199 self.last_reload_timestamp = Instant::now();
200 if !success {
201 self.error = Some("Reload failed".to_string());
202 } else {
203 self.error = None;
204 }
205 }
206
207 pub fn record_reload_with_timing(&mut self, success: bool, elapsed: Duration) {
209 self.reload_count += 1;
210 self.last_reload_timestamp = Instant::now();
211 if !success {
212 self.error = Some("Reload failed".to_string());
213 } else {
214 self.error = None;
215 }
216
217 if success && elapsed.as_millis() > 300 {
219 eprintln!(
220 "Warning: Hot-reload took {}ms (target: <300ms)",
221 elapsed.as_millis()
222 );
223 }
224 }
225
226 pub fn last_reload_latency(&self) -> Duration {
228 self.last_reload_timestamp.elapsed()
229 }
230}
231
232impl<M: UiBindable> Default for HotReloadContext<M> {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[derive(Debug, Clone, Copy)]
240pub struct ReloadPerformanceMetrics {
241 pub reload_count: usize,
243
244 pub last_reload_latency: Duration,
246
247 pub cache_hit_rate: f64,
249
250 pub cache_size: usize,
252}
253
254impl ReloadPerformanceMetrics {
255 pub fn meets_target(&self) -> bool {
257 self.last_reload_latency.as_millis() < 300
258 }
259
260 pub fn latency_ms(&self) -> u128 {
262 self.last_reload_latency.as_millis()
263 }
264}
265
266#[derive(Debug)]
268pub enum ReloadResult<M: UiBindable> {
269 Success(AppState<M>),
271
272 ParseError(ParseError),
274
275 ValidationError(Vec<String>),
277
278 StateRestoreWarning(AppState<M>, String),
280}
281
282pub fn attempt_hot_reload<M, F>(
359 xml_source: &str,
360 current_state: &AppState<M>,
361 context: &mut HotReloadContext<M>,
362 create_handlers: F,
363) -> ReloadResult<M>
364where
365 M: UiBindable + Serialize + DeserializeOwned + Default,
366 F: FnOnce() -> dampen_core::handler::HandlerRegistry,
367{
368 let reload_start = Instant::now();
369
370 if let Err(e) = context.snapshot_model(¤t_state.model) {
372 eprintln!("Warning: Failed to snapshot model: {}", e);
374 }
375
376 let new_document = if let Some(cached_doc) = context.get_cached_document(xml_source) {
378 cached_doc
380 } else {
381 match dampen_core::parser::parse(xml_source) {
383 Ok(doc) => {
384 context.cache_document(xml_source, doc.clone());
385 doc
386 }
387 Err(err) => {
388 context.record_reload(false);
389 return ReloadResult::ParseError(err);
390 }
391 }
392 };
393
394 let new_handlers = create_handlers();
396
397 if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
399 context.record_reload(false);
400 let error_messages: Vec<String> = missing_handlers
401 .iter()
402 .map(|h| format!("Handler '{}' is referenced but not registered", h))
403 .collect();
404 return ReloadResult::ValidationError(error_messages);
405 }
406
407 let restored_model = match context.restore_model() {
409 Ok(model) => {
410 model
412 }
413 Err(e) => {
414 eprintln!("Warning: Failed to restore model ({}), using default", e);
416
417 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
419
420 context.record_reload(true);
421 return ReloadResult::StateRestoreWarning(new_state, e);
422 }
423 };
424
425 let new_state = AppState::with_all(new_document, restored_model, new_handlers);
427
428 let elapsed = reload_start.elapsed();
429 context.record_reload_with_timing(true, elapsed);
430 ReloadResult::Success(new_state)
431}
432
433pub async fn attempt_hot_reload_async<M, F>(
496 xml_source: Arc<String>,
497 current_state: &AppState<M>,
498 context: &mut HotReloadContext<M>,
499 create_handlers: F,
500) -> ReloadResult<M>
501where
502 M: UiBindable + Serialize + DeserializeOwned + Default + Send + 'static,
503 F: FnOnce() -> dampen_core::handler::HandlerRegistry + Send + 'static,
504{
505 let reload_start = Instant::now();
506
507 if let Err(e) = context.snapshot_model(¤t_state.model) {
509 eprintln!("Warning: Failed to snapshot model: {}", e);
510 }
511
512 let model_snapshot = context.last_model_snapshot.clone();
514
515 let new_document = if let Some(cached_doc) = context.get_cached_document(&xml_source) {
517 cached_doc
519 } else {
520 let xml_for_parse = Arc::clone(&xml_source);
522 let parse_result =
523 tokio::task::spawn_blocking(move || dampen_core::parser::parse(&xml_for_parse)).await;
524
525 match parse_result {
526 Ok(Ok(doc)) => {
527 context.cache_document(&xml_source, doc.clone());
528 doc
529 }
530 Ok(Err(err)) => {
531 context.record_reload(false);
532 return ReloadResult::ParseError(err);
533 }
534 Err(join_err) => {
535 context.record_reload(false);
536 let error = ParseError {
537 kind: dampen_core::parser::error::ParseErrorKind::XmlSyntax,
538 span: dampen_core::ir::span::Span::default(),
539 message: format!("Async parsing failed: {}", join_err),
540 suggestion: Some(
541 "Check if the XML file is accessible and not corrupted".to_string(),
542 ),
543 };
544 return ReloadResult::ParseError(error);
545 }
546 }
547 };
548
549 let new_handlers = create_handlers();
551
552 if let Err(missing_handlers) = validate_handlers(&new_document, &new_handlers) {
554 context.record_reload(false);
555 let error_messages: Vec<String> = missing_handlers
556 .iter()
557 .map(|h| format!("Handler '{}' is referenced but not registered", h))
558 .collect();
559 return ReloadResult::ValidationError(error_messages);
560 }
561
562 let restored_model = match model_snapshot {
564 Some(json) => match serde_json::from_str::<M>(&json) {
565 Ok(model) => model,
566 Err(e) => {
567 eprintln!("Warning: Failed to restore model ({}), using default", e);
568 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
569 context.record_reload(true);
570 return ReloadResult::StateRestoreWarning(
571 new_state,
572 format!("Failed to deserialize model: {}", e),
573 );
574 }
575 },
576 None => {
577 eprintln!("Warning: No model snapshot available, using default");
578 let new_state = AppState::with_all(new_document, M::default(), new_handlers);
579 context.record_reload(true);
580 return ReloadResult::StateRestoreWarning(
581 new_state,
582 "No model snapshot available".to_string(),
583 );
584 }
585 };
586
587 let new_state = AppState::with_all(new_document, restored_model, new_handlers);
589
590 let elapsed = reload_start.elapsed();
591 context.record_reload_with_timing(true, elapsed);
592 ReloadResult::Success(new_state)
593}
594
595fn collect_handler_names(document: &dampen_core::ir::DampenDocument) -> Vec<String> {
608 use std::collections::HashSet;
609
610 let mut handlers = HashSet::new();
611 collect_handlers_from_node(&document.root, &mut handlers);
612 handlers.into_iter().collect()
613}
614
615fn collect_handlers_from_node(
617 node: &dampen_core::ir::node::WidgetNode,
618 handlers: &mut std::collections::HashSet<String>,
619) {
620 for event in &node.events {
622 handlers.insert(event.handler.clone());
623 }
624
625 for child in &node.children {
627 collect_handlers_from_node(child, handlers);
628 }
629}
630
631fn validate_handlers(
642 document: &dampen_core::ir::DampenDocument,
643 registry: &dampen_core::handler::HandlerRegistry,
644) -> Result<(), Vec<String>> {
645 let referenced_handlers = collect_handler_names(document);
646 let mut missing_handlers = Vec::new();
647
648 for handler_name in referenced_handlers {
649 if registry.get(&handler_name).is_none() {
650 missing_handlers.push(handler_name);
651 }
652 }
653
654 if missing_handlers.is_empty() {
655 Ok(())
656 } else {
657 Err(missing_handlers)
658 }
659}
660
661#[derive(Debug)]
663pub enum ThemeReloadResult {
664 Success,
666
667 ParseError(String),
669
670 ValidationError(String),
672
673 NoThemeContext,
675
676 FileNotFound,
678}
679
680pub fn attempt_theme_hot_reload(
725 theme_path: &std::path::Path,
726 theme_context: &mut Option<dampen_core::state::ThemeContext>,
727) -> ThemeReloadResult {
728 let theme_context = match theme_context {
729 Some(ctx) => ctx,
730 None => return ThemeReloadResult::NoThemeContext,
731 };
732
733 let content = match std::fs::read_to_string(theme_path) {
734 Ok(c) => c,
735 Err(e) => return ThemeReloadResult::ParseError(format!("Failed to read file: {}", e)),
736 };
737
738 let new_doc = match dampen_core::parser::theme_parser::parse_theme_document(&content) {
739 Ok(doc) => doc,
740 Err(e) => {
741 return ThemeReloadResult::ParseError(format!(
742 "Failed to parse theme document: {}",
743 e.message
744 ));
745 }
746 };
747
748 if let Err(e) = new_doc.validate() {
749 return ThemeReloadResult::ValidationError(e.to_string());
750 }
751
752 theme_context.reload(new_doc);
753 ThemeReloadResult::Success
754}
755
756pub fn is_theme_file_path(path: &std::path::Path) -> bool {
758 path.file_name()
759 .and_then(|n| n.to_str())
760 .map(|n| n == "theme.dampen")
761 .unwrap_or(false)
762}
763
764pub fn get_theme_dir_from_path(path: &std::path::Path) -> Option<std::path::PathBuf> {
769 let path = std::fs::canonicalize(path).ok()?;
770 let theme_file_name = path.file_name()?;
771
772 if theme_file_name == "theme.dampen" {
773 return Some(path.parent()?.to_path_buf());
774 }
775
776 None
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use serde::{Deserialize, Serialize};
783
784 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
785 struct TestModel {
786 count: i32,
787 name: String,
788 }
789
790 impl UiBindable for TestModel {
791 fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> {
792 None
793 }
794
795 fn available_fields() -> Vec<String> {
796 vec![]
797 }
798 }
799
800 impl Default for TestModel {
801 fn default() -> Self {
802 Self {
803 count: 0,
804 name: "default".to_string(),
805 }
806 }
807 }
808
809 #[test]
810 fn test_snapshot_model_success() {
811 let mut context = HotReloadContext::<TestModel>::new();
812 let model = TestModel {
813 count: 42,
814 name: "Alice".to_string(),
815 };
816
817 let result = context.snapshot_model(&model);
818 assert!(result.is_ok());
819 assert!(context.last_model_snapshot.is_some());
820 }
821
822 #[test]
823 fn test_restore_model_success() {
824 let mut context = HotReloadContext::<TestModel>::new();
825 let original = TestModel {
826 count: 42,
827 name: "Alice".to_string(),
828 };
829
830 context.snapshot_model(&original).unwrap();
832
833 let restored = context.restore_model().unwrap();
835 assert_eq!(restored, original);
836 }
837
838 #[test]
839 fn test_restore_model_no_snapshot() {
840 let context = HotReloadContext::<TestModel>::new();
841
842 let result = context.restore_model();
844 assert!(result.is_err());
845 assert!(result.unwrap_err().contains("No model snapshot"));
846 }
847
848 #[test]
849 fn test_snapshot_restore_round_trip() {
850 let mut context = HotReloadContext::<TestModel>::new();
851 let original = TestModel {
852 count: 999,
853 name: "Bob".to_string(),
854 };
855
856 context.snapshot_model(&original).unwrap();
858
859 let mut modified = original.clone();
860 modified.count = 0;
861 modified.name = "Changed".to_string();
862
863 let restored = context.restore_model().unwrap();
865 assert_eq!(restored, original);
866 assert_ne!(restored, modified);
867 }
868
869 #[test]
870 fn test_multiple_snapshots() {
871 let mut context = HotReloadContext::<TestModel>::new();
872
873 let model1 = TestModel {
875 count: 1,
876 name: "First".to_string(),
877 };
878 context.snapshot_model(&model1).unwrap();
879
880 let model2 = TestModel {
882 count: 2,
883 name: "Second".to_string(),
884 };
885 context.snapshot_model(&model2).unwrap();
886
887 let restored = context.restore_model().unwrap();
889 assert_eq!(restored, model2);
890 assert_ne!(restored, model1);
891 }
892
893 #[test]
894 fn test_record_reload() {
895 let mut context = HotReloadContext::<TestModel>::new();
896
897 assert_eq!(context.reload_count, 0);
898 assert!(context.error.is_none());
899
900 context.record_reload(true);
902 assert_eq!(context.reload_count, 1);
903 assert!(context.error.is_none());
904
905 context.record_reload(false);
907 assert_eq!(context.reload_count, 2);
908 assert!(context.error.is_some());
909
910 context.record_reload(true);
912 assert_eq!(context.reload_count, 3);
913 assert!(context.error.is_none());
914 }
915
916 #[test]
917 fn test_cache_hit_rate_calculated_correctly() {
918 use std::sync::atomic::Ordering;
919
920 let mut context = HotReloadContext::<TestModel>::new();
921
922 assert_eq!(context.calculate_cache_hit_rate(), 0.0);
924
925 context.cache_hits.store(3, Ordering::Relaxed);
927 context.cache_misses.store(2, Ordering::Relaxed);
928
929 assert_eq!(context.calculate_cache_hit_rate(), 0.6);
931 }
932
933 #[test]
934 fn test_cache_hit_rate_zero_division() {
935 let context = HotReloadContext::<TestModel>::new();
936 assert_eq!(context.calculate_cache_hit_rate(), 0.0);
938 }
939
940 #[test]
941 fn test_cache_hit_rate_full_misses() {
942 use std::sync::atomic::Ordering;
943
944 let mut context = HotReloadContext::<TestModel>::new();
945 context.cache_hits.store(0, Ordering::Relaxed);
946 context.cache_misses.store(5, Ordering::Relaxed);
947 assert_eq!(context.calculate_cache_hit_rate(), 0.0);
948 }
949
950 #[test]
951 fn test_cache_hit_rate_full_hits() {
952 use std::sync::atomic::Ordering;
953
954 let mut context = HotReloadContext::<TestModel>::new();
955 context.cache_hits.store(5, Ordering::Relaxed);
956 context.cache_misses.store(0, Ordering::Relaxed);
957 assert_eq!(context.calculate_cache_hit_rate(), 1.0);
958 }
959
960 #[test]
961 fn test_attempt_hot_reload_success() {
962 use dampen_core::handler::HandlerRegistry;
963 use dampen_core::parser;
964
965 let xml_v1 =
967 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
968 let doc_v1 = parser::parse(xml_v1).unwrap();
969 let model_v1 = TestModel {
970 count: 42,
971 name: "Alice".to_string(),
972 };
973 let registry_v1 = HandlerRegistry::new();
974 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
975
976 let mut context = HotReloadContext::<TestModel>::new();
978
979 let xml_v2 =
981 r#"<dampen version="1.0"><column><text value="Version 2" /></column></dampen>"#;
982
983 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
985
986 match result {
988 ReloadResult::Success(new_state) => {
989 assert_eq!(new_state.model.count, 42);
990 assert_eq!(new_state.model.name, "Alice");
991 assert_eq!(context.reload_count, 1);
992 }
993 _ => panic!("Expected Success, got {:?}", result),
994 }
995 }
996
997 #[test]
998 fn test_attempt_hot_reload_parse_error() {
999 use dampen_core::handler::HandlerRegistry;
1000 use dampen_core::parser;
1001
1002 let xml_v1 =
1004 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
1005 let doc_v1 = parser::parse(xml_v1).unwrap();
1006 let model_v1 = TestModel {
1007 count: 10,
1008 name: "Bob".to_string(),
1009 };
1010 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1011
1012 let mut context = HotReloadContext::<TestModel>::new();
1013
1014 let xml_invalid = r#"<dampen version="1.0"><column><text value="Broken"#;
1016
1017 let result = attempt_hot_reload(xml_invalid, &state_v1, &mut context, || {
1019 HandlerRegistry::new()
1020 });
1021
1022 match result {
1024 ReloadResult::ParseError(_err) => {
1025 assert_eq!(context.reload_count, 1); }
1028 _ => panic!("Expected ParseError, got {:?}", result),
1029 }
1030 }
1031
1032 #[test]
1033 fn test_attempt_hot_reload_model_restore_failure() {
1034 use dampen_core::handler::HandlerRegistry;
1035 use dampen_core::parser;
1036
1037 let xml_v1 =
1039 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
1040 let doc_v1 = parser::parse(xml_v1).unwrap();
1041 let model_v1 = TestModel {
1042 count: 99,
1043 name: "Charlie".to_string(),
1044 };
1045 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1046
1047 let mut context = HotReloadContext::<TestModel>::new();
1049 context.last_model_snapshot = Some("{ invalid json }".to_string()); let xml_v2 =
1053 r#"<dampen version="1.0"><column><text value="Version 2" /></column></dampen>"#;
1054
1055 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1057
1058 match result {
1065 ReloadResult::Success(new_state) => {
1066 assert_eq!(new_state.model.count, 99);
1068 assert_eq!(new_state.model.name, "Charlie");
1069 assert_eq!(context.reload_count, 1);
1070 }
1071 _ => panic!("Expected Success, got {:?}", result),
1072 }
1073 }
1074
1075 #[test]
1076 fn test_attempt_hot_reload_preserves_model_across_multiple_reloads() {
1077 use dampen_core::handler::HandlerRegistry;
1078 use dampen_core::parser;
1079
1080 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1082 let doc_v1 = parser::parse(xml_v1).unwrap();
1083 let model_v1 = TestModel {
1084 count: 100,
1085 name: "Dave".to_string(),
1086 };
1087 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1088
1089 let mut context = HotReloadContext::<TestModel>::new();
1090
1091 let xml_v2 = r#"<dampen version="1.0"><column><text value="V2" /></column></dampen>"#;
1093 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1094
1095 let state_v2 = match result {
1096 ReloadResult::Success(s) => s,
1097 _ => panic!("First reload failed"),
1098 };
1099
1100 assert_eq!(state_v2.model.count, 100);
1101 assert_eq!(state_v2.model.name, "Dave");
1102
1103 let xml_v3 = r#"<dampen version="1.0"><column><text value="V3" /></column></dampen>"#;
1105 let result = attempt_hot_reload(xml_v3, &state_v2, &mut context, || HandlerRegistry::new());
1106
1107 let state_v3 = match result {
1108 ReloadResult::Success(s) => s,
1109 _ => panic!("Second reload failed"),
1110 };
1111
1112 assert_eq!(state_v3.model.count, 100);
1114 assert_eq!(state_v3.model.name, "Dave");
1115 assert_eq!(context.reload_count, 2);
1116 }
1117
1118 #[test]
1119 fn test_attempt_hot_reload_with_handler_registry() {
1120 use dampen_core::handler::HandlerRegistry;
1121 use dampen_core::parser;
1122
1123 let xml_v1 = r#"<dampen version="1.0"><column><button label="Click" on_click="test" /></column></dampen>"#;
1125 let doc_v1 = parser::parse(xml_v1).unwrap();
1126 let model_v1 = TestModel {
1127 count: 5,
1128 name: "Eve".to_string(),
1129 };
1130
1131 let registry_v1 = HandlerRegistry::new();
1132 registry_v1.register_simple("test", |_model| {
1133 });
1135
1136 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1137
1138 let mut context = HotReloadContext::<TestModel>::new();
1139
1140 let xml_v2 = r#"<dampen version="1.0"><column><button label="Click Me" on_click="test2" /></column></dampen>"#;
1142
1143 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1145 let registry = HandlerRegistry::new();
1146 registry.register_simple("test2", |_model| {
1147 });
1149 registry
1150 });
1151
1152 match result {
1154 ReloadResult::Success(new_state) => {
1155 assert_eq!(new_state.model.count, 5);
1157 assert_eq!(new_state.model.name, "Eve");
1158
1159 assert!(new_state.handler_registry.get("test2").is_some());
1161 }
1162 _ => panic!("Expected Success, got {:?}", result),
1163 }
1164 }
1165
1166 #[test]
1167 fn test_collect_handler_names() {
1168 use dampen_core::parser;
1169
1170 let xml = r#"
1172 <dampen version="1.0">
1173 <column>
1174 <button label="Click" on_click="handle_click" />
1175 <text_input placeholder="Type" on_input="handle_input" />
1176 <button label="Submit" on_click="handle_submit" />
1177 </column>
1178 </dampen>
1179 "#;
1180
1181 let doc = parser::parse(xml).unwrap();
1182 let handlers = collect_handler_names(&doc);
1183
1184 assert_eq!(handlers.len(), 3);
1186 assert!(handlers.contains(&"handle_click".to_string()));
1187 assert!(handlers.contains(&"handle_input".to_string()));
1188 assert!(handlers.contains(&"handle_submit".to_string()));
1189 }
1190
1191 #[test]
1192 fn test_collect_handler_names_nested() {
1193 use dampen_core::parser;
1194
1195 let xml = r#"
1197 <dampen version="1.0">
1198 <column>
1199 <row>
1200 <button label="A" on_click="handler_a" />
1201 </row>
1202 <row>
1203 <button label="B" on_click="handler_b" />
1204 <column>
1205 <button label="C" on_click="handler_c" />
1206 </column>
1207 </row>
1208 </column>
1209 </dampen>
1210 "#;
1211
1212 let doc = parser::parse(xml).unwrap();
1213 let handlers = collect_handler_names(&doc);
1214
1215 assert_eq!(handlers.len(), 3);
1217 assert!(handlers.contains(&"handler_a".to_string()));
1218 assert!(handlers.contains(&"handler_b".to_string()));
1219 assert!(handlers.contains(&"handler_c".to_string()));
1220 }
1221
1222 #[test]
1223 fn test_collect_handler_names_duplicates() {
1224 use dampen_core::parser;
1225
1226 let xml = r#"
1228 <dampen version="1.0">
1229 <column>
1230 <button label="1" on_click="same_handler" />
1231 <button label="2" on_click="same_handler" />
1232 <button label="3" on_click="same_handler" />
1233 </column>
1234 </dampen>
1235 "#;
1236
1237 let doc = parser::parse(xml).unwrap();
1238 let handlers = collect_handler_names(&doc);
1239
1240 assert_eq!(handlers.len(), 1);
1242 assert!(handlers.contains(&"same_handler".to_string()));
1243 }
1244
1245 #[test]
1246 fn test_validate_handlers_all_present() {
1247 use dampen_core::handler::HandlerRegistry;
1248 use dampen_core::parser;
1249
1250 let xml = r#"
1251 <dampen version="1.0">
1252 <column>
1253 <button label="Click" on_click="test_handler" />
1254 </column>
1255 </dampen>
1256 "#;
1257
1258 let doc = parser::parse(xml).unwrap();
1259 let registry = HandlerRegistry::new();
1260 registry.register_simple("test_handler", |_model| {});
1261
1262 let result = validate_handlers(&doc, ®istry);
1263 assert!(result.is_ok());
1264 }
1265
1266 #[test]
1267 fn test_validate_handlers_missing() {
1268 use dampen_core::handler::HandlerRegistry;
1269 use dampen_core::parser;
1270
1271 let xml = r#"
1272 <dampen version="1.0">
1273 <column>
1274 <button label="Click" on_click="missing_handler" />
1275 </column>
1276 </dampen>
1277 "#;
1278
1279 let doc = parser::parse(xml).unwrap();
1280 let registry = HandlerRegistry::new();
1281 let result = validate_handlers(&doc, ®istry);
1284 assert!(result.is_err());
1285
1286 let missing = result.unwrap_err();
1287 assert_eq!(missing.len(), 1);
1288 assert_eq!(missing[0], "missing_handler");
1289 }
1290
1291 #[test]
1292 fn test_validate_handlers_multiple_missing() {
1293 use dampen_core::handler::HandlerRegistry;
1294 use dampen_core::parser;
1295
1296 let xml = r#"
1297 <dampen version="1.0">
1298 <column>
1299 <button label="A" on_click="handler_a" />
1300 <button label="B" on_click="handler_b" />
1301 <button label="C" on_click="handler_c" />
1302 </column>
1303 </dampen>
1304 "#;
1305
1306 let doc = parser::parse(xml).unwrap();
1307 let registry = HandlerRegistry::new();
1308 registry.register_simple("handler_b", |_model| {});
1310
1311 let result = validate_handlers(&doc, ®istry);
1312 assert!(result.is_err());
1313
1314 let missing = result.unwrap_err();
1315 assert_eq!(missing.len(), 2);
1316 assert!(missing.contains(&"handler_a".to_string()));
1317 assert!(missing.contains(&"handler_c".to_string()));
1318 }
1319
1320 #[test]
1321 fn test_attempt_hot_reload_validation_error() {
1322 use dampen_core::handler::HandlerRegistry;
1323 use dampen_core::parser;
1324
1325 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1327 let doc_v1 = parser::parse(xml_v1).unwrap();
1328 let model_v1 = TestModel {
1329 count: 10,
1330 name: "Test".to_string(),
1331 };
1332 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1333
1334 let mut context = HotReloadContext::<TestModel>::new();
1335
1336 let xml_v2 = r#"
1338 <dampen version="1.0">
1339 <column>
1340 <button label="Click" on_click="unregistered_handler" />
1341 </column>
1342 </dampen>
1343 "#;
1344
1345 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1347 HandlerRegistry::new() });
1349
1350 match result {
1352 ReloadResult::ValidationError(errors) => {
1353 assert!(!errors.is_empty());
1354 assert!(errors[0].contains("unregistered_handler"));
1355 assert_eq!(context.reload_count, 1); }
1357 _ => panic!("Expected ValidationError, got {:?}", result),
1358 }
1359 }
1360
1361 #[test]
1362 fn test_attempt_hot_reload_validation_success() {
1363 use dampen_core::handler::HandlerRegistry;
1364 use dampen_core::parser;
1365
1366 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1368 let doc_v1 = parser::parse(xml_v1).unwrap();
1369 let model_v1 = TestModel {
1370 count: 20,
1371 name: "Valid".to_string(),
1372 };
1373 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1374
1375 let mut context = HotReloadContext::<TestModel>::new();
1376
1377 let xml_v2 = r#"
1379 <dampen version="1.0">
1380 <column>
1381 <button label="Click" on_click="registered_handler" />
1382 </column>
1383 </dampen>
1384 "#;
1385
1386 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1388 let registry = HandlerRegistry::new();
1389 registry.register_simple("registered_handler", |_model| {});
1390 registry
1391 });
1392
1393 match result {
1395 ReloadResult::Success(new_state) => {
1396 assert_eq!(new_state.model.count, 20);
1397 assert_eq!(new_state.model.name, "Valid");
1398 assert_eq!(context.reload_count, 1);
1399 }
1400 _ => panic!("Expected Success, got {:?}", result),
1401 }
1402 }
1403
1404 #[test]
1405 fn test_handler_registry_complete_replacement() {
1406 use dampen_core::handler::HandlerRegistry;
1407 use dampen_core::parser;
1408
1409 let xml_v1 = r#"
1411 <dampen version="1.0">
1412 <column>
1413 <button label="Old" on_click="old_handler" />
1414 </column>
1415 </dampen>
1416 "#;
1417 let doc_v1 = parser::parse(xml_v1).unwrap();
1418 let model_v1 = TestModel {
1419 count: 1,
1420 name: "Initial".to_string(),
1421 };
1422
1423 let registry_v1 = HandlerRegistry::new();
1424 registry_v1.register_simple("old_handler", |_model| {});
1425
1426 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1427
1428 assert!(state_v1.handler_registry.get("old_handler").is_some());
1430
1431 let mut context = HotReloadContext::<TestModel>::new();
1432
1433 let xml_v2 = r#"
1435 <dampen version="1.0">
1436 <column>
1437 <button label="New" on_click="new_handler" />
1438 <button label="Another" on_click="another_handler" />
1439 </column>
1440 </dampen>
1441 "#;
1442
1443 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1445 let registry = HandlerRegistry::new();
1446 registry.register_simple("new_handler", |_model| {});
1447 registry.register_simple("another_handler", |_model| {});
1448 registry
1449 });
1450
1451 match result {
1453 ReloadResult::Success(new_state) => {
1454 assert_eq!(new_state.model.count, 1);
1456 assert_eq!(new_state.model.name, "Initial");
1457
1458 assert!(new_state.handler_registry.get("old_handler").is_none());
1460
1461 assert!(new_state.handler_registry.get("new_handler").is_some());
1463 assert!(new_state.handler_registry.get("another_handler").is_some());
1464 }
1465 _ => panic!("Expected Success, got {:?}", result),
1466 }
1467 }
1468
1469 #[test]
1470 fn test_handler_registry_rebuild_before_validation() {
1471 use dampen_core::handler::HandlerRegistry;
1472 use dampen_core::parser;
1473
1474 let xml_v1 = r#"<dampen version="1.0"><column><button on_click="handler_a" label="A" /></column></dampen>"#;
1479 let doc_v1 = parser::parse(xml_v1).unwrap();
1480 let model_v1 = TestModel {
1481 count: 100,
1482 name: "Test".to_string(),
1483 };
1484
1485 let registry_v1 = HandlerRegistry::new();
1486 registry_v1.register_simple("handler_a", |_model| {});
1487
1488 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1489
1490 let mut context = HotReloadContext::<TestModel>::new();
1491
1492 let xml_v2 = r#"<dampen version="1.0"><column><button on_click="handler_b" label="B" /></column></dampen>"#;
1494
1495 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1497 let registry = HandlerRegistry::new();
1498 registry.register_simple("handler_b", |_model| {}); registry
1500 });
1501
1502 match result {
1504 ReloadResult::Success(new_state) => {
1505 assert_eq!(new_state.model.count, 100);
1506 assert!(new_state.handler_registry.get("handler_b").is_some());
1508 assert!(new_state.handler_registry.get("handler_a").is_none());
1510 }
1511 _ => panic!(
1512 "Expected Success (registry rebuilt before validation), got {:?}",
1513 result
1514 ),
1515 }
1516 }
1517}
1518
1519#[cfg(test)]
1520mod theme_reload_tests {
1521 use super::*;
1522 use dampen_core::ir::style::Color;
1523 use dampen_core::ir::theme::{SpacingScale, Theme, ThemeDocument, ThemePalette, Typography};
1524 use tempfile::TempDir;
1525
1526 fn create_test_palette(primary: &str) -> ThemePalette {
1527 ThemePalette {
1528 primary: Some(Color::from_hex(primary).unwrap()),
1529 secondary: Some(Color::from_hex("#2ecc71").unwrap()),
1530 success: Some(Color::from_hex("#27ae60").unwrap()),
1531 warning: Some(Color::from_hex("#f39c12").unwrap()),
1532 danger: Some(Color::from_hex("#e74c3c").unwrap()),
1533 background: Some(Color::from_hex("#ecf0f1").unwrap()),
1534 surface: Some(Color::from_hex("#ffffff").unwrap()),
1535 text: Some(Color::from_hex("#2c3e50").unwrap()),
1536 text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
1537 }
1538 }
1539
1540 fn create_test_theme(name: &str, primary: &str) -> Theme {
1541 Theme {
1542 name: name.to_string(),
1543 palette: create_test_palette(primary),
1544 typography: Typography {
1545 font_family: Some("sans-serif".to_string()),
1546 font_size_base: Some(16.0),
1547 font_size_small: Some(12.0),
1548 font_size_large: Some(24.0),
1549 font_weight: dampen_core::ir::theme::FontWeight::Normal,
1550 line_height: Some(1.5),
1551 },
1552 spacing: SpacingScale { unit: Some(8.0) },
1553 base_styles: std::collections::HashMap::new(),
1554 extends: None,
1555 }
1556 }
1557
1558 fn create_test_document() -> ThemeDocument {
1559 ThemeDocument {
1560 themes: std::collections::HashMap::from([
1561 ("light".to_string(), create_test_theme("light", "#3498db")),
1562 ("dark".to_string(), create_test_theme("dark", "#5dade2")),
1563 ]),
1564 default_theme: Some("light".to_string()),
1565 follow_system: true,
1566 }
1567 }
1568
1569 fn create_test_theme_context() -> dampen_core::state::ThemeContext {
1570 let doc = create_test_document();
1571 dampen_core::state::ThemeContext::from_document(doc, None).unwrap()
1572 }
1573
1574 #[test]
1575 fn test_attempt_theme_hot_reload_success() {
1576 let temp_dir = TempDir::new().unwrap();
1577 let theme_dir = temp_dir.path().join("src/ui/theme");
1578 std::fs::create_dir_all(&theme_dir).unwrap();
1579
1580 let theme_content = r##"
1581 <dampen version="1.0">
1582 <themes>
1583 <theme name="light">
1584 <palette
1585 primary="#111111"
1586 secondary="#2ecc71"
1587 success="#27ae60"
1588 warning="#f39c12"
1589 danger="#e74c3c"
1590 background="#ecf0f1"
1591 surface="#ffffff"
1592 text="#2c3e50"
1593 text_secondary="#7f8c8d" />
1594 </theme>
1595 </themes>
1596 <default_theme name="light" />
1597 </dampen>
1598 "##;
1599
1600 let theme_path = theme_dir.join("theme.dampen");
1601 std::fs::write(&theme_path, theme_content).unwrap();
1602
1603 let mut theme_ctx = Some(create_test_theme_context());
1604 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1605
1606 match result {
1607 ThemeReloadResult::Success => {
1608 let ctx = theme_ctx.unwrap();
1609 assert_eq!(ctx.active_name(), "light");
1610 assert_eq!(
1611 ctx.active().palette.primary,
1612 Some(Color::from_hex("#111111").unwrap())
1613 );
1614 }
1615 _ => panic!("Expected Success, got {:?}", result),
1616 }
1617 }
1618
1619 #[test]
1620 fn test_attempt_theme_hot_reload_parse_error() {
1621 let temp_dir = TempDir::new().unwrap();
1622 let theme_dir = temp_dir.path().join("src/ui/theme");
1623 std::fs::create_dir_all(&theme_dir).unwrap();
1624
1625 let theme_path = theme_dir.join("theme.dampen");
1626 std::fs::write(&theme_path, "invalid xml").unwrap();
1627
1628 let mut theme_ctx = Some(create_test_theme_context());
1629 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1630
1631 match result {
1632 ThemeReloadResult::ParseError(_) => {}
1633 _ => panic!("Expected ParseError, got {:?}", result),
1634 }
1635 }
1636
1637 #[test]
1638 fn test_attempt_theme_hot_reload_no_theme_context() {
1639 let temp_dir = TempDir::new().unwrap();
1640 let theme_dir = temp_dir.path().join("src/ui/theme");
1641 std::fs::create_dir_all(&theme_dir).unwrap();
1642
1643 let theme_path = theme_dir.join("theme.dampen");
1644 std::fs::write(
1645 &theme_path,
1646 r#"<dampen version="1.0"><themes></themes></dampen>"#,
1647 )
1648 .unwrap();
1649
1650 let mut theme_ctx: Option<dampen_core::state::ThemeContext> = None;
1651 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1652
1653 match result {
1654 ThemeReloadResult::NoThemeContext => {}
1655 _ => panic!("Expected NoThemeContext, got {:?}", result),
1656 }
1657 }
1658
1659 #[test]
1660 fn test_attempt_theme_hot_reload_preserves_active_theme() {
1661 let temp_dir = TempDir::new().unwrap();
1662 let theme_dir = temp_dir.path().join("src/ui/theme");
1663 std::fs::create_dir_all(&theme_dir).unwrap();
1664
1665 let theme_content = r##"
1666 <dampen version="1.0">
1667 <themes>
1668 <theme name="new_theme">
1669 <palette
1670 primary="#ff0000"
1671 secondary="#2ecc71"
1672 success="#27ae60"
1673 warning="#f39c12"
1674 danger="#e74c3c"
1675 background="#ecf0f1"
1676 surface="#ffffff"
1677 text="#2c3e50"
1678 text_secondary="#7f8c8d" />
1679 </theme>
1680 <theme name="dark">
1681 <palette
1682 primary="#5dade2"
1683 secondary="#52be80"
1684 success="#27ae60"
1685 warning="#f39c12"
1686 danger="#ec7063"
1687 background="#2c3e50"
1688 surface="#34495e"
1689 text="#ecf0f1"
1690 text_secondary="#95a5a6" />
1691 </theme>
1692 </themes>
1693 <default_theme name="new_theme" />
1694 </dampen>
1695 "##;
1696
1697 let theme_path = theme_dir.join("theme.dampen");
1698 std::fs::write(&theme_path, theme_content).unwrap();
1699
1700 let mut theme_ctx = Some(create_test_theme_context());
1701 theme_ctx.as_mut().unwrap().set_theme("dark").unwrap();
1702 assert_eq!(theme_ctx.as_ref().unwrap().active_name(), "dark");
1703
1704 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1705
1706 match result {
1707 ThemeReloadResult::Success => {
1708 let ctx = theme_ctx.unwrap();
1709 assert_eq!(ctx.active_name(), "dark");
1710 }
1711 _ => panic!("Expected Success, got {:?}", result),
1712 }
1713 }
1714
1715 #[test]
1716 fn test_is_theme_file_path() {
1717 assert!(is_theme_file_path(&std::path::PathBuf::from(
1718 "src/ui/theme/theme.dampen"
1719 )));
1720 assert!(!is_theme_file_path(&std::path::PathBuf::from(
1721 "src/ui/window.dampen"
1722 )));
1723 assert!(is_theme_file_path(&std::path::PathBuf::from(
1724 "/some/path/theme.dampen"
1725 )));
1726 }
1727
1728 #[test]
1729 fn test_get_theme_dir_from_path() {
1730 let temp_dir = TempDir::new().unwrap();
1731 let theme_dir = temp_dir.path().join("src/ui/theme");
1732 std::fs::create_dir_all(&theme_dir).unwrap();
1733 let theme_path = theme_dir.join("theme.dampen");
1734 std::fs::write(
1735 &theme_path,
1736 "<dampen version=\"1.0\"><themes></themes></dampen>",
1737 )
1738 .unwrap();
1739
1740 let result = get_theme_dir_from_path(&theme_path);
1741 assert!(result.is_some());
1742 assert_eq!(result.unwrap(), theme_dir);
1743 }
1744}