minijinja_autoreload/
lib.rs

1//! This crate adds a convenient auto reloader for MiniJinja.
2//!
3//! The [`AutoReloader`] is an utility type that can be passed around or placed
4//! in a global variable using something like
5//! [`once_cell`](https://docs.rs/once_cell/latest/once_cell/).  It accepts a
6//! closure which is used to create an environment which is passed a notifier.
7//! This notifier can automatically watch file system paths or it can be manually
8//! instructed to invalidate the environment.
9//!
10//! Every time [`acquire_env`](AutoReloader::acquire_env) is called the reloader
11//! checks if a reload is scheduled in which case it will automatically re-create
12//! the environment.  While the [guard](EnvironmentGuard) is retained, the environment
13//! won't perform further reloads.
14//!
15//! ## Example
16//!
17//! This is an example that uses the `source` feature of MiniJinja to automatically
18//! load templates from the file system:
19//!
20//! ```
21//! # fn test() -> Result<(), minijinja::Error> {
22//! use minijinja_autoreload::AutoReloader;
23//! use minijinja::{Environment, path_loader};
24//!
25//! let reloader = AutoReloader::new(|notifier| {
26//!     let template_path = "path/to/templates";
27//!     let mut env = Environment::new();
28//!     env.set_loader(path_loader(template_path));
29//!     notifier.watch_path(template_path, true);
30//!     Ok(env)
31//! });
32//!
33//! let env = reloader.acquire_env()?;
34//! let tmpl = env.get_template("index.html")?;
35//! # Ok(()) } fn main() { test().unwrap_err(); }
36//! ```
37#![cfg_attr(docsrs, feature(doc_cfg))]
38#![deny(missing_docs)]
39use std::ops::Deref;
40use std::sync::{Arc, Mutex, MutexGuard, Weak};
41
42#[cfg(feature = "watch-fs")]
43use std::path::Path;
44
45use minijinja::{Environment, Error};
46
47type EnvCreator = dyn Fn(Notifier) -> Result<Environment<'static>, Error> + Send + Sync + 'static;
48
49/// An auto reloader for MiniJinja [`Environment`]s.
50pub struct AutoReloader {
51    env_creator: Box<EnvCreator>,
52    notifier: Notifier,
53    cached_env: Mutex<Option<Environment<'static>>>,
54}
55
56impl AutoReloader {
57    /// Creates a new auto reloader.
58    ///
59    /// The given closure is invoked to create a new environment whenever the auto-reloader
60    /// detects that it should reload.  It is passed a [`Notifier`] which can be used to
61    /// signal back to the auto-reloader when the environment should be re-created.
62    pub fn new<F>(f: F) -> AutoReloader
63    where
64        F: Fn(Notifier) -> Result<Environment<'static>, Error> + Send + Sync + 'static,
65    {
66        AutoReloader {
67            env_creator: Box::new(f),
68            notifier: Notifier::new(),
69            cached_env: Default::default(),
70        }
71    }
72
73    /// Returns a handle to the notifier.
74    ///
75    /// This handle can be cloned and used for instance to trigger reloads from
76    /// a background thread.
77    pub fn notifier(&self) -> Notifier {
78        self.notifier.weak()
79    }
80
81    /// Acquires a new environment, potentially reloading it if needed.
82    ///
83    /// The acquired environment is protected by a guard.  Until the guard is
84    /// dropped the environment won't be reloaded.  Crucially the environment
85    /// returned is also behind a shared reference which means that it won't
86    /// be possible to mutate it.
87    ///
88    /// If the creator function passed to the constructor fails, the error is
89    /// returned from this method.
90    pub fn acquire_env(&self) -> Result<EnvironmentGuard<'_>, Error> {
91        let mut mutex_guard = self.cached_env.lock().unwrap();
92        if mutex_guard.is_none() || self.notifier.should_reload() {
93            let weak_notifier = self.notifier.prepare_and_mark_reload()?;
94            if mutex_guard.is_none() || !self.notifier.fast_reload() {
95                *mutex_guard = Some((self.env_creator)(weak_notifier)?);
96            } else {
97                mutex_guard.as_mut().unwrap().clear_templates();
98            }
99        }
100        Ok(EnvironmentGuard { mutex_guard })
101    }
102}
103
104/// A guard that de-references into an [`Environment`].
105///
106/// While the guard is in scope, auto reloads are temporarily paused until the
107/// guard is dropped.
108pub struct EnvironmentGuard<'reloader> {
109    mutex_guard: MutexGuard<'reloader, Option<Environment<'static>>>,
110}
111
112impl Deref for EnvironmentGuard<'_> {
113    type Target = Environment<'static>;
114
115    fn deref(&self) -> &Self::Target {
116        self.mutex_guard.as_ref().unwrap()
117    }
118}
119
120/// Signalling utility to notify the auto reloader about reloads.
121///
122/// The notifier can both watch file system paths or be manually instructed
123/// to reload.  For file system path watching the `watch-fs` feature must be
124/// enabled.
125///
126/// The notifier can be cloned which allows it to be passed to background
127/// threads.  If the [`AutoReloader`] that created the notifier was dropped
128/// the notifier itself is marked as dead.  In that case it stops doing anything
129/// useful and returns `true` from [`is_dead`](Self::is_dead).
130#[derive(Clone)]
131pub struct Notifier {
132    handle: NotifierImplHandle,
133}
134
135#[derive(Clone)]
136enum NotifierImplHandle {
137    Weak(Weak<Mutex<NotifierImpl>>),
138    Strong(Arc<Mutex<NotifierImpl>>),
139}
140
141#[derive(Default)]
142struct NotifierImpl {
143    should_reload: bool,
144    should_reload_callback: Option<Box<dyn Fn() -> bool + Send + Sync + 'static>>,
145    on_should_reload_callback: Option<Box<dyn Fn() + Send + Sync + 'static>>,
146    fast_reload: bool,
147    #[cfg(feature = "watch-fs")]
148    fs_watcher: Option<notify::RecommendedWatcher>,
149    #[cfg(feature = "watch-fs")]
150    persistent_fs_watcher: bool,
151}
152
153impl Notifier {
154    fn new() -> Notifier {
155        Notifier {
156            handle: NotifierImplHandle::Strong(Arc::new(Default::default())),
157        }
158    }
159
160    /// Tells the notifier that the environment needs reloading.
161    pub fn request_reload(&self) {
162        if let Some(handle) = self.handle() {
163            handle.lock().unwrap().should_reload = true;
164
165            if let Some(callback) = handle.lock().unwrap().on_should_reload_callback.as_ref() {
166                callback();
167            }
168        }
169    }
170
171    /// Enables or disables fast reload.
172    ///
173    /// By default fast reload is disabled which causes the entire environment to
174    /// be recreated.  When fast reload is enabled, then on reload
175    /// [`clear_templates`](minijinja::Environment::clear_templates) is called.
176    /// This will only work if a loader was added to the environment as the loader
177    /// will then cause templates to be loaded again.
178    ///
179    /// When fast reloading is enabled, the environment creation function is
180    /// only called once.
181    pub fn set_fast_reload(&self, yes: bool) {
182        if let Some(handle) = self.handle() {
183            handle.lock().unwrap().fast_reload = yes;
184        }
185    }
186
187    /// Registers a callback that is invoked to check the freshness of the
188    /// environment.
189    ///
190    /// When the auto reloader checks if it should reload it will optionally
191    /// invoke this callback.  Only one callback can be set.  If this is invoked
192    /// another time, the old callback is removed.  The function should return
193    /// `true` to request a reload, `false` otherwise.
194    pub fn set_callback<F>(&self, f: F)
195    where
196        F: Fn() -> bool + Send + Sync + 'static,
197    {
198        if let Some(handle) = self.handle() {
199            handle.lock().unwrap().should_reload_callback = Some(Box::new(f));
200        }
201    }
202
203    /// Registers a callback that is invoked when the environment should reload.
204    ///
205    /// The callback is called in these scenarios:
206    ///  - A reload was requested via [`request_reload`](Self::request_reload)
207    ///  - The callback set via [`set_callback`](Self::set_callback) returned `true`
208    ///
209    /// When the feature `watch-fs` is enabled, the callback is also invoked when
210    /// a change is detected in a watched file path.
211    ///
212    /// **NOTE**: This callback is invoked **before** the environment is reloaded.
213    pub fn set_on_should_reload_callback<F>(&self, f: F)
214    where
215        F: Fn() + Send + Sync + 'static,
216    {
217        if let Some(handle) = self.handle() {
218            handle.lock().unwrap().on_should_reload_callback = Some(Box::new(f));
219        }
220    }
221
222    /// Tells the notifier to watch a file system path for changes.
223    ///
224    /// This can watch both directories and files.  The second parameter controls if
225    /// the watcher should be operating recursively in which case `true` must be passed.
226    /// When the environment is reloaded the watcher is cleared out which means that
227    /// [`watch_path`](Self::watch_path) must be invoked again.  If this is not wanted
228    /// [`persistent_watch`](Self::persistent_watch) must be enabled.
229    #[cfg(feature = "watch-fs")]
230    #[cfg_attr(docsrs, doc(cfg(feature = "watch-fs")))]
231    pub fn watch_path<P: AsRef<Path>>(&self, path: P, recursive: bool) {
232        use notify::{RecursiveMode, Watcher};
233        let path = path.as_ref();
234        let mode = if recursive {
235            RecursiveMode::Recursive
236        } else {
237            RecursiveMode::NonRecursive
238        };
239        self.with_fs_watcher(|watcher| {
240            watcher.watch(path, mode).ok();
241        });
242    }
243
244    /// Tells the notifier to stop watching a file system path for changes.
245    ///
246    /// This is usually not useful but it can be useful when [persistent
247    /// watching](Self::persistent_watch) is enabled.
248    #[cfg(feature = "watch-fs")]
249    #[cfg_attr(docsrs, doc(cfg(feature = "watch-fs")))]
250    pub fn unwatch_path<P: AsRef<Path>>(&self, path: P) {
251        use notify::Watcher;
252        let path = path.as_ref();
253        self.with_fs_watcher(|watcher| {
254            watcher.unwatch(path).ok();
255        });
256    }
257
258    /// Enables the file system watcher to be persistent between reloads.
259    #[cfg(feature = "watch-fs")]
260    #[cfg_attr(docsrs, doc(cfg(feature = "watch-fs")))]
261    pub fn persistent_watch(&self, yes: bool) {
262        if let Some(handle) = self.handle() {
263            handle.lock().unwrap().persistent_fs_watcher = yes;
264        }
265    }
266
267    /// Returns `true` if the notifier is dead.
268    ///
269    /// A notifier is dead when the [`AutoReloader`] that created it was dropped.
270    pub fn is_dead(&self) -> bool {
271        self.handle().is_none()
272    }
273
274    fn handle(&self) -> Option<Arc<Mutex<NotifierImpl>>> {
275        match self.handle {
276            NotifierImplHandle::Weak(ref weak) => weak.upgrade(),
277            NotifierImplHandle::Strong(ref arc) => Some(arc.clone()),
278        }
279    }
280
281    fn fast_reload(&self) -> bool {
282        let Some(handle) = self.handle() else {
283            return false;
284        };
285        let inner = handle.lock().unwrap();
286        inner.fast_reload
287    }
288
289    fn should_reload(&self) -> bool {
290        let Some(handle) = self.handle() else {
291            return false;
292        };
293        let inner = handle.lock().unwrap();
294
295        // Early return if we already know we should reload so that
296        // `should_reload_callback` isn't polled unnecessarily and
297        // `on_should_reload_callback` isn't called twice.
298        // (It should've been called already when setting `should_reload`.)
299        if inner.should_reload {
300            return true;
301        }
302
303        let should_reload = inner.should_reload_callback.as_ref().is_some_and(|x| x());
304
305        if should_reload {
306            if let Some(callback) = inner.on_should_reload_callback.as_ref() {
307                callback();
308            }
309        }
310
311        should_reload
312    }
313
314    #[cfg(feature = "watch-fs")]
315    fn with_fs_watcher<F: FnOnce(&mut notify::RecommendedWatcher)>(&self, f: F) {
316        use notify::event::{EventKind, ModifyKind};
317
318        let Some(handle) = self.handle() else {
319            return;
320        };
321        let weak_handle = Arc::downgrade(&handle);
322        f(handle
323            .lock()
324            .unwrap()
325            .fs_watcher
326            .get_or_insert_with(move || {
327                notify::recommended_watcher(move |res: notify::Result<notify::Event>| {
328                    let kind = match res {
329                        Ok(event) => event.kind,
330                        Err(_) => return,
331                    };
332                    if matches!(
333                        kind,
334                        EventKind::Create(_)
335                            | EventKind::Remove(_)
336                            | EventKind::Modify(
337                                ModifyKind::Data(_) | ModifyKind::Name(_) | ModifyKind::Any
338                            )
339                    ) {
340                        if let Some(inner) = weak_handle.upgrade() {
341                            inner.lock().unwrap().should_reload = true;
342
343                            if let Some(callback) =
344                                inner.lock().unwrap().on_should_reload_callback.as_ref()
345                            {
346                                callback();
347                            }
348                        }
349                    }
350                })
351                .expect("unable to initialize fs watcher")
352            }));
353    }
354
355    fn prepare_and_mark_reload(&self) -> Result<Notifier, Error> {
356        let handle = self.handle().expect("notifier unexpectedly went away");
357        #[cfg(feature = "watch-fs")]
358        {
359            let mut locked_handle = handle.lock().unwrap();
360            if !locked_handle.persistent_fs_watcher && !locked_handle.fast_reload {
361                locked_handle.fs_watcher.take();
362            }
363        }
364        let weak_notifier = Notifier {
365            handle: NotifierImplHandle::Weak(Arc::downgrade(&handle)),
366        };
367        handle.lock().unwrap().should_reload = false;
368        Ok(weak_notifier)
369    }
370
371    fn weak(&self) -> Notifier {
372        let handle = self.handle().expect("notifier unexpectedly went away");
373        Notifier {
374            handle: NotifierImplHandle::Weak(Arc::downgrade(&handle)),
375        }
376    }
377}