mockforge_graphql/
schema_watcher.rs

1//! Schema hot-reloading with file watching
2//!
3//! Watches GraphQL schema files for changes and automatically reloads them.
4
5use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
6use std::path::PathBuf;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tracing::{error, info, warn};
10
11/// Schema file watcher that monitors for changes
12pub struct SchemaWatcher {
13    /// Path to the schema file
14    schema_path: PathBuf,
15    /// Current schema SDL
16    schema_sdl: Arc<RwLock<String>>,
17    /// File watcher
18    _watcher: Option<RecommendedWatcher>,
19}
20
21impl SchemaWatcher {
22    /// Create a new schema watcher
23    pub async fn new(
24        schema_path: PathBuf,
25    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
26        // Load initial schema
27        let initial_sdl = tokio::fs::read_to_string(&schema_path).await?;
28        let schema_sdl = Arc::new(RwLock::new(initial_sdl));
29
30        Ok(Self {
31            schema_path,
32            schema_sdl,
33            _watcher: None,
34        })
35    }
36
37    /// Start watching the schema file for changes
38    pub fn start_watching(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
39        let schema_path = self.schema_path.clone();
40        let schema_sdl = Arc::clone(&self.schema_sdl);
41
42        let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
43            match res {
44                Ok(event) => {
45                    if event.kind.is_modify() {
46                        info!("Schema file changed, reloading...");
47                        let path = schema_path.clone();
48                        let sdl = Arc::clone(&schema_sdl);
49
50                        // Spawn async task to reload schema
51                        tokio::spawn(async move {
52                            match tokio::fs::read_to_string(&path).await {
53                                Ok(new_sdl) => {
54                                    let mut sdl_lock = sdl.write().await;
55                                    *sdl_lock = new_sdl;
56                                    info!("✓ Schema reloaded successfully");
57                                }
58                                Err(e) => {
59                                    error!("Failed to reload schema: {}", e);
60                                }
61                            }
62                        });
63                    }
64                }
65                Err(e) => warn!("Watch error: {:?}", e),
66            }
67        })?;
68
69        watcher.watch(&self.schema_path, RecursiveMode::NonRecursive)?;
70        self._watcher = Some(watcher);
71
72        info!("👀 Watching schema file: {:?}", self.schema_path);
73        Ok(())
74    }
75
76    /// Get the current schema SDL
77    pub async fn get_schema(&self) -> String {
78        self.schema_sdl.read().await.clone()
79    }
80
81    /// Manually reload the schema
82    pub async fn reload(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
83        let new_sdl = tokio::fs::read_to_string(&self.schema_path).await?;
84        let mut sdl_lock = self.schema_sdl.write().await;
85        *sdl_lock = new_sdl;
86        info!("Schema manually reloaded");
87        Ok(())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use std::io::Write;
95    use tempfile::NamedTempFile;
96
97    #[tokio::test]
98    async fn test_schema_watcher_creation() {
99        let mut temp_file = NamedTempFile::new().unwrap();
100        writeln!(temp_file, "type Query {{ hello: String }}").unwrap();
101
102        let watcher = SchemaWatcher::new(temp_file.path().to_path_buf()).await;
103        assert!(watcher.is_ok());
104    }
105
106    #[tokio::test]
107    async fn test_get_schema() {
108        let mut temp_file = NamedTempFile::new().unwrap();
109        let schema_content = "type Query { hello: String }";
110        writeln!(temp_file, "{}", schema_content).unwrap();
111
112        let watcher = SchemaWatcher::new(temp_file.path().to_path_buf()).await.unwrap();
113        let sdl = watcher.get_schema().await;
114        assert!(sdl.contains("type Query"));
115    }
116
117    #[tokio::test]
118    async fn test_manual_reload() {
119        let mut temp_file = NamedTempFile::new().unwrap();
120        writeln!(temp_file, "type Query {{ hello: String }}").unwrap();
121
122        let watcher = SchemaWatcher::new(temp_file.path().to_path_buf()).await.unwrap();
123
124        // Modify the file
125        writeln!(temp_file, "type Query {{ world: String }}").unwrap();
126
127        // Manually reload
128        let result = watcher.reload().await;
129        assert!(result.is_ok());
130
131        let sdl = watcher.get_schema().await;
132        assert!(sdl.contains("world"));
133    }
134}