Skip to main content

goud_engine/assets/hot_reload/
config.rs

1//! Configuration types for the hot reload system.
2
3use std::collections::HashSet;
4use std::path::Path;
5use std::time::Duration;
6
7// =============================================================================
8// HotReloadConfig
9// =============================================================================
10
11/// Configuration for hot reloading behavior.
12#[derive(Debug, Clone)]
13pub struct HotReloadConfig {
14    /// Whether hot reloading is enabled (default: true in debug, false in release).
15    pub enabled: bool,
16
17    /// Debounce delay to avoid duplicate reloads (default: 100ms).
18    ///
19    /// File systems often emit multiple events for a single change.
20    /// This delay groups rapid changes together.
21    pub debounce_duration: Duration,
22
23    /// Whether to watch subdirectories recursively (default: true).
24    pub recursive: bool,
25
26    /// File extensions to watch (empty = watch all files).
27    pub extensions: HashSet<String>,
28
29    /// Whether to ignore hidden files (starting with '.').
30    pub ignore_hidden: bool,
31
32    /// Whether to ignore temporary files (ending with '~', '.tmp', '.swp', etc.).
33    pub ignore_temp: bool,
34}
35
36impl HotReloadConfig {
37    /// Creates a new configuration with default values.
38    pub fn new() -> Self {
39        Self {
40            enabled: cfg!(debug_assertions), // Enabled in debug builds by default
41            debounce_duration: Duration::from_millis(100),
42            recursive: true,
43            extensions: HashSet::new(), // Empty = watch all
44            ignore_hidden: true,
45            ignore_temp: true,
46        }
47    }
48
49    /// Sets whether hot reloading is enabled.
50    pub fn with_enabled(mut self, enabled: bool) -> Self {
51        self.enabled = enabled;
52        self
53    }
54
55    /// Sets the debounce duration.
56    pub fn with_debounce(mut self, duration: Duration) -> Self {
57        self.debounce_duration = duration;
58        self
59    }
60
61    /// Sets whether to watch recursively.
62    pub fn with_recursive(mut self, recursive: bool) -> Self {
63        self.recursive = recursive;
64        self
65    }
66
67    /// Adds a file extension to watch (e.g., "png", "json").
68    pub fn watch_extension(mut self, ext: impl Into<String>) -> Self {
69        self.extensions.insert(ext.into());
70        self
71    }
72
73    /// Sets whether to ignore hidden files.
74    pub fn with_ignore_hidden(mut self, ignore: bool) -> Self {
75        self.ignore_hidden = ignore;
76        self
77    }
78
79    /// Sets whether to ignore temporary files.
80    pub fn with_ignore_temp(mut self, ignore: bool) -> Self {
81        self.ignore_temp = ignore;
82        self
83    }
84
85    /// Returns true if a path should be watched based on configuration.
86    pub fn should_watch(&self, path: &Path) -> bool {
87        // Check hidden files
88        if self.ignore_hidden {
89            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
90                if name.starts_with('.') {
91                    return false;
92                }
93            }
94        }
95
96        // Check temporary files
97        if self.ignore_temp {
98            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
99                if name.ends_with('~')
100                    || name.ends_with(".tmp")
101                    || name.ends_with(".swp")
102                    || name.ends_with(".bak")
103                {
104                    return false;
105                }
106            }
107        }
108
109        // Check extension filter
110        if !self.extensions.is_empty() {
111            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
112                if !self.extensions.contains(ext) {
113                    return false;
114                }
115            } else {
116                // No extension and we have filters = ignore
117                return false;
118            }
119        }
120
121        true
122    }
123}
124
125impl Default for HotReloadConfig {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131// =============================================================================
132// Tests
133// =============================================================================
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_default() {
141        let config = HotReloadConfig::default();
142        assert_eq!(config.enabled, cfg!(debug_assertions));
143        assert_eq!(config.debounce_duration, Duration::from_millis(100));
144        assert!(config.recursive);
145        assert!(config.ignore_hidden);
146        assert!(config.ignore_temp);
147        assert!(config.extensions.is_empty());
148    }
149
150    #[test]
151    fn test_with_enabled() {
152        let config = HotReloadConfig::new().with_enabled(false);
153        assert!(!config.enabled);
154    }
155
156    #[test]
157    fn test_with_debounce() {
158        let config = HotReloadConfig::new().with_debounce(Duration::from_millis(500));
159        assert_eq!(config.debounce_duration, Duration::from_millis(500));
160    }
161
162    #[test]
163    fn test_with_recursive() {
164        let config = HotReloadConfig::new().with_recursive(false);
165        assert!(!config.recursive);
166    }
167
168    #[test]
169    fn test_watch_extension() {
170        let config = HotReloadConfig::new()
171            .watch_extension("png")
172            .watch_extension("json");
173
174        assert!(config.extensions.contains("png"));
175        assert!(config.extensions.contains("json"));
176        assert_eq!(config.extensions.len(), 2);
177    }
178
179    #[test]
180    fn test_should_watch_all() {
181        let config = HotReloadConfig::new();
182
183        // No extension filter = watch all
184        assert!(config.should_watch(Path::new("test.png")));
185        assert!(config.should_watch(Path::new("test.json")));
186    }
187
188    #[test]
189    fn test_should_watch_with_extension_filter() {
190        let config = HotReloadConfig::new().watch_extension("png");
191
192        assert!(config.should_watch(Path::new("test.png")));
193        assert!(!config.should_watch(Path::new("test.json")));
194        assert!(!config.should_watch(Path::new("no_extension")));
195    }
196
197    #[test]
198    fn test_should_watch_hidden_files() {
199        let config = HotReloadConfig::new().with_ignore_hidden(true);
200
201        assert!(config.should_watch(Path::new("test.png")));
202        assert!(!config.should_watch(Path::new(".hidden.png")));
203    }
204
205    #[test]
206    fn test_should_watch_temp_files() {
207        let config = HotReloadConfig::new().with_ignore_temp(true);
208
209        assert!(config.should_watch(Path::new("test.png")));
210        assert!(!config.should_watch(Path::new("test.png~")));
211        assert!(!config.should_watch(Path::new("test.tmp")));
212        assert!(!config.should_watch(Path::new("test.swp")));
213        assert!(!config.should_watch(Path::new("test.bak")));
214    }
215
216    #[test]
217    fn test_should_watch_allow_hidden() {
218        let config = HotReloadConfig::new().with_ignore_hidden(false);
219
220        assert!(config.should_watch(Path::new(".hidden.png")));
221    }
222
223    #[test]
224    fn test_clone() {
225        let config = HotReloadConfig::new().watch_extension("png");
226        let cloned = config.clone();
227
228        assert_eq!(config.enabled, cloned.enabled);
229        assert_eq!(config.extensions.len(), cloned.extensions.len());
230    }
231
232    #[test]
233    fn test_debug() {
234        let config = HotReloadConfig::new();
235        let debug = format!("{:?}", config);
236        assert!(debug.contains("HotReloadConfig"));
237    }
238}