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}