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#[derive(Debug)]
638pub enum ThemeReloadResult {
639 Success,
641
642 ParseError(String),
644
645 ValidationError(String),
647
648 NoThemeContext,
650
651 FileNotFound,
653}
654
655pub fn attempt_theme_hot_reload(
700 theme_path: &std::path::Path,
701 theme_context: &mut Option<dampen_core::state::ThemeContext>,
702) -> ThemeReloadResult {
703 let theme_context = match theme_context {
704 Some(ctx) => ctx,
705 None => return ThemeReloadResult::NoThemeContext,
706 };
707
708 let content = match std::fs::read_to_string(theme_path) {
709 Ok(c) => c,
710 Err(e) => return ThemeReloadResult::ParseError(format!("Failed to read file: {}", e)),
711 };
712
713 let new_doc = match dampen_core::parser::theme_parser::parse_theme_document(&content) {
714 Ok(doc) => doc,
715 Err(e) => {
716 return ThemeReloadResult::ParseError(format!(
717 "Failed to parse theme document: {}",
718 e.message
719 ));
720 }
721 };
722
723 if let Err(e) = new_doc.validate() {
724 return ThemeReloadResult::ValidationError(e.to_string());
725 }
726
727 theme_context.reload(new_doc);
728 ThemeReloadResult::Success
729}
730
731pub fn is_theme_file_path(path: &std::path::Path) -> bool {
733 path.file_name()
734 .and_then(|n| n.to_str())
735 .map(|n| n == "theme.dampen")
736 .unwrap_or(false)
737}
738
739pub fn get_theme_dir_from_path(path: &std::path::Path) -> Option<std::path::PathBuf> {
744 let path = std::fs::canonicalize(path).ok()?;
745 let theme_file_name = path.file_name()?;
746
747 if theme_file_name == "theme.dampen" {
748 return Some(path.parent()?.to_path_buf());
749 }
750
751 None
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757 use serde::{Deserialize, Serialize};
758
759 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
760 struct TestModel {
761 count: i32,
762 name: String,
763 }
764
765 impl UiBindable for TestModel {
766 fn get_field(&self, _path: &[&str]) -> Option<dampen_core::binding::BindingValue> {
767 None
768 }
769
770 fn available_fields() -> Vec<String> {
771 vec![]
772 }
773 }
774
775 impl Default for TestModel {
776 fn default() -> Self {
777 Self {
778 count: 0,
779 name: "default".to_string(),
780 }
781 }
782 }
783
784 #[test]
785 fn test_snapshot_model_success() {
786 let mut context = HotReloadContext::<TestModel>::new();
787 let model = TestModel {
788 count: 42,
789 name: "Alice".to_string(),
790 };
791
792 let result = context.snapshot_model(&model);
793 assert!(result.is_ok());
794 assert!(context.last_model_snapshot.is_some());
795 }
796
797 #[test]
798 fn test_restore_model_success() {
799 let mut context = HotReloadContext::<TestModel>::new();
800 let original = TestModel {
801 count: 42,
802 name: "Alice".to_string(),
803 };
804
805 context.snapshot_model(&original).unwrap();
807
808 let restored = context.restore_model().unwrap();
810 assert_eq!(restored, original);
811 }
812
813 #[test]
814 fn test_restore_model_no_snapshot() {
815 let context = HotReloadContext::<TestModel>::new();
816
817 let result = context.restore_model();
819 assert!(result.is_err());
820 assert!(result.unwrap_err().contains("No model snapshot"));
821 }
822
823 #[test]
824 fn test_snapshot_restore_round_trip() {
825 let mut context = HotReloadContext::<TestModel>::new();
826 let original = TestModel {
827 count: 999,
828 name: "Bob".to_string(),
829 };
830
831 context.snapshot_model(&original).unwrap();
833
834 let mut modified = original.clone();
835 modified.count = 0;
836 modified.name = "Changed".to_string();
837
838 let restored = context.restore_model().unwrap();
840 assert_eq!(restored, original);
841 assert_ne!(restored, modified);
842 }
843
844 #[test]
845 fn test_multiple_snapshots() {
846 let mut context = HotReloadContext::<TestModel>::new();
847
848 let model1 = TestModel {
850 count: 1,
851 name: "First".to_string(),
852 };
853 context.snapshot_model(&model1).unwrap();
854
855 let model2 = TestModel {
857 count: 2,
858 name: "Second".to_string(),
859 };
860 context.snapshot_model(&model2).unwrap();
861
862 let restored = context.restore_model().unwrap();
864 assert_eq!(restored, model2);
865 assert_ne!(restored, model1);
866 }
867
868 #[test]
869 fn test_record_reload() {
870 let mut context = HotReloadContext::<TestModel>::new();
871
872 assert_eq!(context.reload_count, 0);
873 assert!(context.error.is_none());
874
875 context.record_reload(true);
877 assert_eq!(context.reload_count, 1);
878 assert!(context.error.is_none());
879
880 context.record_reload(false);
882 assert_eq!(context.reload_count, 2);
883 assert!(context.error.is_some());
884
885 context.record_reload(true);
887 assert_eq!(context.reload_count, 3);
888 assert!(context.error.is_none());
889 }
890
891 #[test]
892 fn test_attempt_hot_reload_success() {
893 use dampen_core::handler::HandlerRegistry;
894 use dampen_core::parser;
895
896 let xml_v1 =
898 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
899 let doc_v1 = parser::parse(xml_v1).unwrap();
900 let model_v1 = TestModel {
901 count: 42,
902 name: "Alice".to_string(),
903 };
904 let registry_v1 = HandlerRegistry::new();
905 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
906
907 let mut context = HotReloadContext::<TestModel>::new();
909
910 let xml_v2 =
912 r#"<dampen version="1.0"><column><text value="Version 2" /></column></dampen>"#;
913
914 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
916
917 match result {
919 ReloadResult::Success(new_state) => {
920 assert_eq!(new_state.model.count, 42);
921 assert_eq!(new_state.model.name, "Alice");
922 assert_eq!(context.reload_count, 1);
923 }
924 _ => panic!("Expected Success, got {:?}", result),
925 }
926 }
927
928 #[test]
929 fn test_attempt_hot_reload_parse_error() {
930 use dampen_core::handler::HandlerRegistry;
931 use dampen_core::parser;
932
933 let xml_v1 =
935 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
936 let doc_v1 = parser::parse(xml_v1).unwrap();
937 let model_v1 = TestModel {
938 count: 10,
939 name: "Bob".to_string(),
940 };
941 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
942
943 let mut context = HotReloadContext::<TestModel>::new();
944
945 let xml_invalid = r#"<dampen version="1.0"><column><text value="Broken"#;
947
948 let result = attempt_hot_reload(xml_invalid, &state_v1, &mut context, || {
950 HandlerRegistry::new()
951 });
952
953 match result {
955 ReloadResult::ParseError(_err) => {
956 assert_eq!(context.reload_count, 1); }
959 _ => panic!("Expected ParseError, got {:?}", result),
960 }
961 }
962
963 #[test]
964 fn test_attempt_hot_reload_model_restore_failure() {
965 use dampen_core::handler::HandlerRegistry;
966 use dampen_core::parser;
967
968 let xml_v1 =
970 r#"<dampen version="1.0"><column><text value="Version 1" /></column></dampen>"#;
971 let doc_v1 = parser::parse(xml_v1).unwrap();
972 let model_v1 = TestModel {
973 count: 99,
974 name: "Charlie".to_string(),
975 };
976 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
977
978 let mut context = HotReloadContext::<TestModel>::new();
980 context.last_model_snapshot = Some("{ invalid json }".to_string()); let xml_v2 =
984 r#"<dampen version="1.0"><column><text value="Version 2" /></column></dampen>"#;
985
986 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
988
989 match result {
996 ReloadResult::Success(new_state) => {
997 assert_eq!(new_state.model.count, 99);
999 assert_eq!(new_state.model.name, "Charlie");
1000 assert_eq!(context.reload_count, 1);
1001 }
1002 _ => panic!("Expected Success, got {:?}", result),
1003 }
1004 }
1005
1006 #[test]
1007 fn test_attempt_hot_reload_preserves_model_across_multiple_reloads() {
1008 use dampen_core::handler::HandlerRegistry;
1009 use dampen_core::parser;
1010
1011 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1013 let doc_v1 = parser::parse(xml_v1).unwrap();
1014 let model_v1 = TestModel {
1015 count: 100,
1016 name: "Dave".to_string(),
1017 };
1018 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1019
1020 let mut context = HotReloadContext::<TestModel>::new();
1021
1022 let xml_v2 = r#"<dampen version="1.0"><column><text value="V2" /></column></dampen>"#;
1024 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || HandlerRegistry::new());
1025
1026 let state_v2 = match result {
1027 ReloadResult::Success(s) => s,
1028 _ => panic!("First reload failed"),
1029 };
1030
1031 assert_eq!(state_v2.model.count, 100);
1032 assert_eq!(state_v2.model.name, "Dave");
1033
1034 let xml_v3 = r#"<dampen version="1.0"><column><text value="V3" /></column></dampen>"#;
1036 let result = attempt_hot_reload(xml_v3, &state_v2, &mut context, || HandlerRegistry::new());
1037
1038 let state_v3 = match result {
1039 ReloadResult::Success(s) => s,
1040 _ => panic!("Second reload failed"),
1041 };
1042
1043 assert_eq!(state_v3.model.count, 100);
1045 assert_eq!(state_v3.model.name, "Dave");
1046 assert_eq!(context.reload_count, 2);
1047 }
1048
1049 #[test]
1050 fn test_attempt_hot_reload_with_handler_registry() {
1051 use dampen_core::handler::HandlerRegistry;
1052 use dampen_core::parser;
1053
1054 let xml_v1 = r#"<dampen version="1.0"><column><button label="Click" on_click="test" /></column></dampen>"#;
1056 let doc_v1 = parser::parse(xml_v1).unwrap();
1057 let model_v1 = TestModel {
1058 count: 5,
1059 name: "Eve".to_string(),
1060 };
1061
1062 let registry_v1 = HandlerRegistry::new();
1063 registry_v1.register_simple("test", |_model| {
1064 });
1066
1067 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1068
1069 let mut context = HotReloadContext::<TestModel>::new();
1070
1071 let xml_v2 = r#"<dampen version="1.0"><column><button label="Click Me" on_click="test2" /></column></dampen>"#;
1073
1074 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1076 let registry = HandlerRegistry::new();
1077 registry.register_simple("test2", |_model| {
1078 });
1080 registry
1081 });
1082
1083 match result {
1085 ReloadResult::Success(new_state) => {
1086 assert_eq!(new_state.model.count, 5);
1088 assert_eq!(new_state.model.name, "Eve");
1089
1090 assert!(new_state.handler_registry.get("test2").is_some());
1092 }
1093 _ => panic!("Expected Success, got {:?}", result),
1094 }
1095 }
1096
1097 #[test]
1098 fn test_collect_handler_names() {
1099 use dampen_core::parser;
1100
1101 let xml = r#"
1103 <dampen version="1.0">
1104 <column>
1105 <button label="Click" on_click="handle_click" />
1106 <text_input placeholder="Type" on_input="handle_input" />
1107 <button label="Submit" on_click="handle_submit" />
1108 </column>
1109 </dampen>
1110 "#;
1111
1112 let doc = parser::parse(xml).unwrap();
1113 let handlers = collect_handler_names(&doc);
1114
1115 assert_eq!(handlers.len(), 3);
1117 assert!(handlers.contains(&"handle_click".to_string()));
1118 assert!(handlers.contains(&"handle_input".to_string()));
1119 assert!(handlers.contains(&"handle_submit".to_string()));
1120 }
1121
1122 #[test]
1123 fn test_collect_handler_names_nested() {
1124 use dampen_core::parser;
1125
1126 let xml = r#"
1128 <dampen version="1.0">
1129 <column>
1130 <row>
1131 <button label="A" on_click="handler_a" />
1132 </row>
1133 <row>
1134 <button label="B" on_click="handler_b" />
1135 <column>
1136 <button label="C" on_click="handler_c" />
1137 </column>
1138 </row>
1139 </column>
1140 </dampen>
1141 "#;
1142
1143 let doc = parser::parse(xml).unwrap();
1144 let handlers = collect_handler_names(&doc);
1145
1146 assert_eq!(handlers.len(), 3);
1148 assert!(handlers.contains(&"handler_a".to_string()));
1149 assert!(handlers.contains(&"handler_b".to_string()));
1150 assert!(handlers.contains(&"handler_c".to_string()));
1151 }
1152
1153 #[test]
1154 fn test_collect_handler_names_duplicates() {
1155 use dampen_core::parser;
1156
1157 let xml = r#"
1159 <dampen version="1.0">
1160 <column>
1161 <button label="1" on_click="same_handler" />
1162 <button label="2" on_click="same_handler" />
1163 <button label="3" on_click="same_handler" />
1164 </column>
1165 </dampen>
1166 "#;
1167
1168 let doc = parser::parse(xml).unwrap();
1169 let handlers = collect_handler_names(&doc);
1170
1171 assert_eq!(handlers.len(), 1);
1173 assert!(handlers.contains(&"same_handler".to_string()));
1174 }
1175
1176 #[test]
1177 fn test_validate_handlers_all_present() {
1178 use dampen_core::handler::HandlerRegistry;
1179 use dampen_core::parser;
1180
1181 let xml = r#"
1182 <dampen version="1.0">
1183 <column>
1184 <button label="Click" on_click="test_handler" />
1185 </column>
1186 </dampen>
1187 "#;
1188
1189 let doc = parser::parse(xml).unwrap();
1190 let registry = HandlerRegistry::new();
1191 registry.register_simple("test_handler", |_model| {});
1192
1193 let result = validate_handlers(&doc, ®istry);
1194 assert!(result.is_ok());
1195 }
1196
1197 #[test]
1198 fn test_validate_handlers_missing() {
1199 use dampen_core::handler::HandlerRegistry;
1200 use dampen_core::parser;
1201
1202 let xml = r#"
1203 <dampen version="1.0">
1204 <column>
1205 <button label="Click" on_click="missing_handler" />
1206 </column>
1207 </dampen>
1208 "#;
1209
1210 let doc = parser::parse(xml).unwrap();
1211 let registry = HandlerRegistry::new();
1212 let result = validate_handlers(&doc, ®istry);
1215 assert!(result.is_err());
1216
1217 let missing = result.unwrap_err();
1218 assert_eq!(missing.len(), 1);
1219 assert_eq!(missing[0], "missing_handler");
1220 }
1221
1222 #[test]
1223 fn test_validate_handlers_multiple_missing() {
1224 use dampen_core::handler::HandlerRegistry;
1225 use dampen_core::parser;
1226
1227 let xml = r#"
1228 <dampen version="1.0">
1229 <column>
1230 <button label="A" on_click="handler_a" />
1231 <button label="B" on_click="handler_b" />
1232 <button label="C" on_click="handler_c" />
1233 </column>
1234 </dampen>
1235 "#;
1236
1237 let doc = parser::parse(xml).unwrap();
1238 let registry = HandlerRegistry::new();
1239 registry.register_simple("handler_b", |_model| {});
1241
1242 let result = validate_handlers(&doc, ®istry);
1243 assert!(result.is_err());
1244
1245 let missing = result.unwrap_err();
1246 assert_eq!(missing.len(), 2);
1247 assert!(missing.contains(&"handler_a".to_string()));
1248 assert!(missing.contains(&"handler_c".to_string()));
1249 }
1250
1251 #[test]
1252 fn test_attempt_hot_reload_validation_error() {
1253 use dampen_core::handler::HandlerRegistry;
1254 use dampen_core::parser;
1255
1256 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1258 let doc_v1 = parser::parse(xml_v1).unwrap();
1259 let model_v1 = TestModel {
1260 count: 10,
1261 name: "Test".to_string(),
1262 };
1263 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1264
1265 let mut context = HotReloadContext::<TestModel>::new();
1266
1267 let xml_v2 = r#"
1269 <dampen version="1.0">
1270 <column>
1271 <button label="Click" on_click="unregistered_handler" />
1272 </column>
1273 </dampen>
1274 "#;
1275
1276 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1278 HandlerRegistry::new() });
1280
1281 match result {
1283 ReloadResult::ValidationError(errors) => {
1284 assert!(!errors.is_empty());
1285 assert!(errors[0].contains("unregistered_handler"));
1286 assert_eq!(context.reload_count, 1); }
1288 _ => panic!("Expected ValidationError, got {:?}", result),
1289 }
1290 }
1291
1292 #[test]
1293 fn test_attempt_hot_reload_validation_success() {
1294 use dampen_core::handler::HandlerRegistry;
1295 use dampen_core::parser;
1296
1297 let xml_v1 = r#"<dampen version="1.0"><column><text value="V1" /></column></dampen>"#;
1299 let doc_v1 = parser::parse(xml_v1).unwrap();
1300 let model_v1 = TestModel {
1301 count: 20,
1302 name: "Valid".to_string(),
1303 };
1304 let state_v1 = AppState::with_all(doc_v1, model_v1, HandlerRegistry::new());
1305
1306 let mut context = HotReloadContext::<TestModel>::new();
1307
1308 let xml_v2 = r#"
1310 <dampen version="1.0">
1311 <column>
1312 <button label="Click" on_click="registered_handler" />
1313 </column>
1314 </dampen>
1315 "#;
1316
1317 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1319 let registry = HandlerRegistry::new();
1320 registry.register_simple("registered_handler", |_model| {});
1321 registry
1322 });
1323
1324 match result {
1326 ReloadResult::Success(new_state) => {
1327 assert_eq!(new_state.model.count, 20);
1328 assert_eq!(new_state.model.name, "Valid");
1329 assert_eq!(context.reload_count, 1);
1330 }
1331 _ => panic!("Expected Success, got {:?}", result),
1332 }
1333 }
1334
1335 #[test]
1336 fn test_handler_registry_complete_replacement() {
1337 use dampen_core::handler::HandlerRegistry;
1338 use dampen_core::parser;
1339
1340 let xml_v1 = r#"
1342 <dampen version="1.0">
1343 <column>
1344 <button label="Old" on_click="old_handler" />
1345 </column>
1346 </dampen>
1347 "#;
1348 let doc_v1 = parser::parse(xml_v1).unwrap();
1349 let model_v1 = TestModel {
1350 count: 1,
1351 name: "Initial".to_string(),
1352 };
1353
1354 let registry_v1 = HandlerRegistry::new();
1355 registry_v1.register_simple("old_handler", |_model| {});
1356
1357 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1358
1359 assert!(state_v1.handler_registry.get("old_handler").is_some());
1361
1362 let mut context = HotReloadContext::<TestModel>::new();
1363
1364 let xml_v2 = r#"
1366 <dampen version="1.0">
1367 <column>
1368 <button label="New" on_click="new_handler" />
1369 <button label="Another" on_click="another_handler" />
1370 </column>
1371 </dampen>
1372 "#;
1373
1374 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1376 let registry = HandlerRegistry::new();
1377 registry.register_simple("new_handler", |_model| {});
1378 registry.register_simple("another_handler", |_model| {});
1379 registry
1380 });
1381
1382 match result {
1384 ReloadResult::Success(new_state) => {
1385 assert_eq!(new_state.model.count, 1);
1387 assert_eq!(new_state.model.name, "Initial");
1388
1389 assert!(new_state.handler_registry.get("old_handler").is_none());
1391
1392 assert!(new_state.handler_registry.get("new_handler").is_some());
1394 assert!(new_state.handler_registry.get("another_handler").is_some());
1395 }
1396 _ => panic!("Expected Success, got {:?}", result),
1397 }
1398 }
1399
1400 #[test]
1401 fn test_handler_registry_rebuild_before_validation() {
1402 use dampen_core::handler::HandlerRegistry;
1403 use dampen_core::parser;
1404
1405 let xml_v1 = r#"<dampen version="1.0"><column><button on_click="handler_a" label="A" /></column></dampen>"#;
1410 let doc_v1 = parser::parse(xml_v1).unwrap();
1411 let model_v1 = TestModel {
1412 count: 100,
1413 name: "Test".to_string(),
1414 };
1415
1416 let registry_v1 = HandlerRegistry::new();
1417 registry_v1.register_simple("handler_a", |_model| {});
1418
1419 let state_v1 = AppState::with_all(doc_v1, model_v1, registry_v1);
1420
1421 let mut context = HotReloadContext::<TestModel>::new();
1422
1423 let xml_v2 = r#"<dampen version="1.0"><column><button on_click="handler_b" label="B" /></column></dampen>"#;
1425
1426 let result = attempt_hot_reload(xml_v2, &state_v1, &mut context, || {
1428 let registry = HandlerRegistry::new();
1429 registry.register_simple("handler_b", |_model| {}); registry
1431 });
1432
1433 match result {
1435 ReloadResult::Success(new_state) => {
1436 assert_eq!(new_state.model.count, 100);
1437 assert!(new_state.handler_registry.get("handler_b").is_some());
1439 assert!(new_state.handler_registry.get("handler_a").is_none());
1441 }
1442 _ => panic!(
1443 "Expected Success (registry rebuilt before validation), got {:?}",
1444 result
1445 ),
1446 }
1447 }
1448}
1449
1450#[cfg(test)]
1451mod theme_reload_tests {
1452 use super::*;
1453 use dampen_core::ir::style::Color;
1454 use dampen_core::ir::theme::{SpacingScale, Theme, ThemeDocument, ThemePalette, Typography};
1455 use tempfile::TempDir;
1456
1457 fn create_test_palette(primary: &str) -> ThemePalette {
1458 ThemePalette {
1459 primary: Some(Color::from_hex(primary).unwrap()),
1460 secondary: Some(Color::from_hex("#2ecc71").unwrap()),
1461 success: Some(Color::from_hex("#27ae60").unwrap()),
1462 warning: Some(Color::from_hex("#f39c12").unwrap()),
1463 danger: Some(Color::from_hex("#e74c3c").unwrap()),
1464 background: Some(Color::from_hex("#ecf0f1").unwrap()),
1465 surface: Some(Color::from_hex("#ffffff").unwrap()),
1466 text: Some(Color::from_hex("#2c3e50").unwrap()),
1467 text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
1468 }
1469 }
1470
1471 fn create_test_theme(name: &str, primary: &str) -> Theme {
1472 Theme {
1473 name: name.to_string(),
1474 palette: create_test_palette(primary),
1475 typography: Typography {
1476 font_family: Some("sans-serif".to_string()),
1477 font_size_base: Some(16.0),
1478 font_size_small: Some(12.0),
1479 font_size_large: Some(24.0),
1480 font_weight: dampen_core::ir::theme::FontWeight::Normal,
1481 line_height: Some(1.5),
1482 },
1483 spacing: SpacingScale { unit: Some(8.0) },
1484 base_styles: std::collections::HashMap::new(),
1485 extends: None,
1486 }
1487 }
1488
1489 fn create_test_document() -> ThemeDocument {
1490 ThemeDocument {
1491 themes: std::collections::HashMap::from([
1492 ("light".to_string(), create_test_theme("light", "#3498db")),
1493 ("dark".to_string(), create_test_theme("dark", "#5dade2")),
1494 ]),
1495 default_theme: Some("light".to_string()),
1496 follow_system: true,
1497 }
1498 }
1499
1500 fn create_test_theme_context() -> dampen_core::state::ThemeContext {
1501 let doc = create_test_document();
1502 dampen_core::state::ThemeContext::from_document(doc, None).unwrap()
1503 }
1504
1505 #[test]
1506 fn test_attempt_theme_hot_reload_success() {
1507 let temp_dir = TempDir::new().unwrap();
1508 let theme_dir = temp_dir.path().join("src/ui/theme");
1509 std::fs::create_dir_all(&theme_dir).unwrap();
1510
1511 let theme_content = r##"
1512 <dampen version="1.0">
1513 <themes>
1514 <theme name="light">
1515 <palette
1516 primary="#111111"
1517 secondary="#2ecc71"
1518 success="#27ae60"
1519 warning="#f39c12"
1520 danger="#e74c3c"
1521 background="#ecf0f1"
1522 surface="#ffffff"
1523 text="#2c3e50"
1524 text_secondary="#7f8c8d" />
1525 </theme>
1526 </themes>
1527 <default_theme name="light" />
1528 </dampen>
1529 "##;
1530
1531 let theme_path = theme_dir.join("theme.dampen");
1532 std::fs::write(&theme_path, theme_content).unwrap();
1533
1534 let mut theme_ctx = Some(create_test_theme_context());
1535 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1536
1537 match result {
1538 ThemeReloadResult::Success => {
1539 let ctx = theme_ctx.unwrap();
1540 assert_eq!(ctx.active_name(), "light");
1541 assert_eq!(
1542 ctx.active().palette.primary,
1543 Some(Color::from_hex("#111111").unwrap())
1544 );
1545 }
1546 _ => panic!("Expected Success, got {:?}", result),
1547 }
1548 }
1549
1550 #[test]
1551 fn test_attempt_theme_hot_reload_parse_error() {
1552 let temp_dir = TempDir::new().unwrap();
1553 let theme_dir = temp_dir.path().join("src/ui/theme");
1554 std::fs::create_dir_all(&theme_dir).unwrap();
1555
1556 let theme_path = theme_dir.join("theme.dampen");
1557 std::fs::write(&theme_path, "invalid xml").unwrap();
1558
1559 let mut theme_ctx = Some(create_test_theme_context());
1560 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1561
1562 match result {
1563 ThemeReloadResult::ParseError(_) => {}
1564 _ => panic!("Expected ParseError, got {:?}", result),
1565 }
1566 }
1567
1568 #[test]
1569 fn test_attempt_theme_hot_reload_no_theme_context() {
1570 let temp_dir = TempDir::new().unwrap();
1571 let theme_dir = temp_dir.path().join("src/ui/theme");
1572 std::fs::create_dir_all(&theme_dir).unwrap();
1573
1574 let theme_path = theme_dir.join("theme.dampen");
1575 std::fs::write(
1576 &theme_path,
1577 r#"<dampen version="1.0"><themes></themes></dampen>"#,
1578 )
1579 .unwrap();
1580
1581 let mut theme_ctx: Option<dampen_core::state::ThemeContext> = None;
1582 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1583
1584 match result {
1585 ThemeReloadResult::NoThemeContext => {}
1586 _ => panic!("Expected NoThemeContext, got {:?}", result),
1587 }
1588 }
1589
1590 #[test]
1591 fn test_attempt_theme_hot_reload_preserves_active_theme() {
1592 let temp_dir = TempDir::new().unwrap();
1593 let theme_dir = temp_dir.path().join("src/ui/theme");
1594 std::fs::create_dir_all(&theme_dir).unwrap();
1595
1596 let theme_content = r##"
1597 <dampen version="1.0">
1598 <themes>
1599 <theme name="new_theme">
1600 <palette
1601 primary="#ff0000"
1602 secondary="#2ecc71"
1603 success="#27ae60"
1604 warning="#f39c12"
1605 danger="#e74c3c"
1606 background="#ecf0f1"
1607 surface="#ffffff"
1608 text="#2c3e50"
1609 text_secondary="#7f8c8d" />
1610 </theme>
1611 <theme name="dark">
1612 <palette
1613 primary="#5dade2"
1614 secondary="#52be80"
1615 success="#27ae60"
1616 warning="#f39c12"
1617 danger="#ec7063"
1618 background="#2c3e50"
1619 surface="#34495e"
1620 text="#ecf0f1"
1621 text_secondary="#95a5a6" />
1622 </theme>
1623 </themes>
1624 <default_theme name="new_theme" />
1625 </dampen>
1626 "##;
1627
1628 let theme_path = theme_dir.join("theme.dampen");
1629 std::fs::write(&theme_path, theme_content).unwrap();
1630
1631 let mut theme_ctx = Some(create_test_theme_context());
1632 theme_ctx.as_mut().unwrap().set_theme("dark").unwrap();
1633 assert_eq!(theme_ctx.as_ref().unwrap().active_name(), "dark");
1634
1635 let result = attempt_theme_hot_reload(&theme_path, &mut theme_ctx);
1636
1637 match result {
1638 ThemeReloadResult::Success => {
1639 let ctx = theme_ctx.unwrap();
1640 assert_eq!(ctx.active_name(), "dark");
1641 }
1642 _ => panic!("Expected Success, got {:?}", result),
1643 }
1644 }
1645
1646 #[test]
1647 fn test_is_theme_file_path() {
1648 assert!(is_theme_file_path(&std::path::PathBuf::from(
1649 "src/ui/theme/theme.dampen"
1650 )));
1651 assert!(!is_theme_file_path(&std::path::PathBuf::from(
1652 "src/ui/window.dampen"
1653 )));
1654 assert!(is_theme_file_path(&std::path::PathBuf::from(
1655 "/some/path/theme.dampen"
1656 )));
1657 }
1658
1659 #[test]
1660 fn test_get_theme_dir_from_path() {
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 let theme_path = theme_dir.join("theme.dampen");
1665 std::fs::write(
1666 &theme_path,
1667 "<dampen version=\"1.0\"><themes></themes></dampen>",
1668 )
1669 .unwrap();
1670
1671 let result = get_theme_dir_from_path(&theme_path);
1672 assert!(result.is_some());
1673 assert_eq!(result.unwrap(), theme_dir);
1674 }
1675}