use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, info, warn};
pub struct SchemaWatcher {
schema_path: PathBuf,
schema_sdl: Arc<RwLock<String>>,
_watcher: Option<RecommendedWatcher>,
}
impl SchemaWatcher {
pub async fn new(
schema_path: PathBuf,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let initial_sdl = tokio::fs::read_to_string(&schema_path).await?;
let schema_sdl = Arc::new(RwLock::new(initial_sdl));
Ok(Self {
schema_path,
schema_sdl,
_watcher: None,
})
}
pub fn start_watching(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let schema_path = self.schema_path.clone();
let schema_sdl = Arc::clone(&self.schema_sdl);
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
match res {
Ok(event) => {
if event.kind.is_modify() {
info!("Schema file changed, reloading...");
let path = schema_path.clone();
let sdl = Arc::clone(&schema_sdl);
tokio::spawn(async move {
match tokio::fs::read_to_string(&path).await {
Ok(new_sdl) => {
let mut sdl_lock = sdl.write().await;
*sdl_lock = new_sdl;
info!("✓ Schema reloaded successfully");
}
Err(e) => {
error!("Failed to reload schema: {}", e);
}
}
});
}
}
Err(e) => warn!("Watch error: {:?}", e),
}
})?;
watcher.watch(&self.schema_path, RecursiveMode::NonRecursive)?;
self._watcher = Some(watcher);
info!("👀 Watching schema file: {:?}", self.schema_path);
Ok(())
}
pub async fn get_schema(&self) -> String {
self.schema_sdl.read().await.clone()
}
pub async fn reload(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let new_sdl = tokio::fs::read_to_string(&self.schema_path).await?;
let mut sdl_lock = self.schema_sdl.write().await;
*sdl_lock = new_sdl;
info!("Schema manually reloaded");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_schema_watcher_creation() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "type Query {{ hello: String }}").unwrap();
let watcher = SchemaWatcher::new(temp_file.path().to_path_buf()).await;
assert!(watcher.is_ok());
}
#[tokio::test]
async fn test_get_schema() {
let mut temp_file = NamedTempFile::new().unwrap();
let schema_content = "type Query { hello: String }";
writeln!(temp_file, "{}", schema_content).unwrap();
let watcher = SchemaWatcher::new(temp_file.path().to_path_buf()).await.unwrap();
let sdl = watcher.get_schema().await;
assert!(sdl.contains("type Query"));
}
#[tokio::test]
async fn test_manual_reload() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "type Query {{ hello: String }}").unwrap();
let watcher = SchemaWatcher::new(temp_file.path().to_path_buf()).await.unwrap();
writeln!(temp_file, "type Query {{ world: String }}").unwrap();
let result = watcher.reload().await;
assert!(result.is_ok());
let sdl = watcher.get_schema().await;
assert!(sdl.contains("world"));
}
}