dynpatch-watcher 0.1.0

File watching and live config reloading for dynpatch
Documentation
//! # dynpatch-watcher
//!
//! File watching and live config reloading for dynpatch.
//!
//! This crate provides utilities for watching files and automatically reloading
//! configurations or patches when they change on disk.

use notify::{
    event::{Event, EventKind},
    RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
};
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::{debug, error, info, warn};

pub mod config;

pub use config::{ConfigWatcher, HotConfig};

#[derive(Error, Debug)]
pub enum WatchError {
    #[error("Failed to watch path: {path}")]
    WatchFailed {
        path: PathBuf,
        #[source]
        source: notify::Error,
    },

    #[error("Failed to read file: {0}")]
    ReadFailed(#[from] std::io::Error),

    #[error("Failed to parse config: {0}")]
    ParseFailed(String),

    #[error("Invalid path: {0}")]
    InvalidPath(PathBuf),
}

pub type Result<T> = std::result::Result<T, WatchError>;

/// File watcher for arbitrary file changes
pub struct FileWatcher {
    _watcher: RecommendedWatcher,
    path: PathBuf,
}

impl FileWatcher {
    /// Create a new file watcher with a callback
    pub fn new<P, F>(path: P, mut callback: F) -> Result<Self>
    where
        P: AsRef<Path>,
        F: FnMut(&Path) + Send + 'static,
    {
        let path = path.as_ref().to_path_buf();
        
        if !path.exists() {
            warn!("Watch path does not exist yet: {:?}", path);
        }

        let watch_path = path.clone();
        let mut watcher = notify::recommended_watcher(move |res: notify::Result<Event>| {
            match res {
                Ok(event) => {
                    if matches!(
                        event.kind,
                        EventKind::Modify(_) | EventKind::Create(_)
                    ) {
                        debug!("File change detected: {:?}", event.paths);
                        for p in &event.paths {
                            callback(p);
                        }
                    }
                }
                Err(e) => error!("Watch error: {:?}", e),
            }
        })
        .map_err(|e| WatchError::WatchFailed {
            path: path.clone(),
            source: e,
        })?;

        watcher
            .watch(&watch_path, RecursiveMode::NonRecursive)
            .map_err(|e| WatchError::WatchFailed {
                path: path.clone(),
                source: e,
            })?;

        info!("Watching file: {:?}", path);

        Ok(Self {
            _watcher: watcher,
            path,
        })
    }

    pub fn path(&self) -> &Path {
        &self.path
    }
}

/// Watch a file and call a callback on changes
pub fn watch<P, F>(path: P, callback: F) -> Result<FileWatcher>
where
    P: AsRef<Path>,
    F: FnMut(&Path) + Send + 'static,
{
    FileWatcher::new(path, callback)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;
    use tempfile::NamedTempFile;

    #[test]
    fn test_watcher_creation() {
        let temp = NamedTempFile::new().unwrap();
        let count = Arc::new(AtomicUsize::new(0));
        let count_clone = count.clone();

        let _watcher = FileWatcher::new(temp.path(), move |_| {
            count_clone.fetch_add(1, Ordering::SeqCst);
        });

        // Watcher should be created successfully
        assert!(_watcher.is_ok());
    }
}