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 let path: PathBuf = path.into();
23 let watch_path = path.parent().ok_or(Error::NoParent)?.to_path_buf();
24
25 let config = Arc::new(C::default());
27 let config_clone = config.clone();
28
29 Self::reload(&config, &path)?;
31
32 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 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 let mut buffer = String::new();
102 file.read_to_string(&mut buffer).map_err(Error::FileRead)?;
103 Ok(buffer)
104}