Skip to main content

hotreload/
lib.rs

1use notify::{RecommendedWatcher, RecursiveMode, Watcher};
2use std::io::{ErrorKind, Read};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6pub trait Reload {
7    type Data: serde::de::DeserializeOwned;
8    fn apply(&self, data: Self::Data) -> Result<(), Box<dyn std::error::Error>>;
9}
10
11pub struct Hotreload<C> {
12    config: Arc<C>,
13    _watcher: RecommendedWatcher,
14}
15
16impl<C> Hotreload<C>
17where
18    C: Reload + Default + Send + Sync + 'static,
19{
20    pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self, Error> {
21        // Get path of the config file containing directory
22        let path: PathBuf = path.into();
23        let watch_path = path.parent().ok_or(Error::NoParent)?.to_path_buf();
24
25        // Init config type
26        let config = Arc::new(C::default());
27        let config_clone = config.clone();
28
29        // Load config file
30        Self::reload(&config, &path)?;
31
32        // Create & Start file watcher
33        type NotifyRes = notify::Result<notify::Event>;
34        let mut watcher = notify::recommended_watcher(move |res: NotifyRes| match res {
35            Ok(event) => {
36                if event.paths.len() == 1
37                    && event.paths[0] == path
38                    && (event.kind.is_modify() || event.kind.is_create())
39                {
40                    #[allow(clippy::collapsible_if)]
41                    if let Err(error) = Self::reload(&config_clone, &path) {
42                        eprintln!("Failed to hotreload config: {}", error);
43                    }
44                }
45            }
46            Err(error) => eprintln!("Hotreload watch error: {}", error),
47        })?;
48        watcher.watch(&watch_path, RecursiveMode::NonRecursive)?;
49
50        Ok(Self {
51            config,
52            _watcher: watcher,
53        })
54    }
55
56    pub fn config(&self) -> &Arc<C> {
57        &self.config
58    }
59
60    fn reload<P: AsRef<Path>>(config: &C, path: P) -> Result<(), Error> {
61        let file = load_file(path)?;
62        let data = toml::from_str(&file).map_err(Error::Deserialize)?;
63        config.apply(data).map_err(Error::Apply)
64    }
65}
66
67#[derive(Debug, thiserror::Error)]
68pub enum Error {
69    #[error("Config file not found: {0}")]
70    NotFound(#[source] std::io::Error),
71    #[error("Config file permission denied: {0}")]
72    PermissionDenied(#[source] std::io::Error),
73    #[error("Failed to read config file: {0}")]
74    FileRead(#[source] std::io::Error),
75    #[error("IO error: {0}")]
76    Io(#[from] std::io::Error),
77    #[error("Failed to deserialize config TOML: {0}")]
78    Deserialize(#[from] toml::de::Error),
79    #[error("Notify error: {0}")]
80    Notify(#[from] notify::Error),
81    #[error("Path doesn't have a parent")]
82    NoParent,
83    #[error("Failed to apply new config: {0}")]
84    Apply(#[source] Box<dyn std::error::Error>),
85}
86
87fn load_file<P: AsRef<Path>>(path: P) -> Result<String, Error> {
88    // Open file
89    let mut file = match std::fs::File::open(path) {
90        Ok(file) => file,
91        Err(error) => {
92            return Err(match error.kind() {
93                ErrorKind::NotFound => Error::NotFound(error),
94                ErrorKind::PermissionDenied => Error::PermissionDenied(error),
95                _ => Error::Io(error),
96            });
97        }
98    };
99
100    // Read content
101    let mut buffer = String::new();
102    file.read_to_string(&mut buffer).map_err(Error::FileRead)?;
103    Ok(buffer)
104}