liquid_core/partials/
lazy.rs

1use std::collections::HashMap;
2use std::fmt;
3use std::sync;
4
5use crate::error::Result;
6use crate::parser;
7use crate::parser::Language;
8use crate::runtime;
9use crate::runtime::PartialStore;
10use crate::runtime::Renderable;
11
12use super::PartialCompiler;
13use super::PartialSource;
14
15/// An lazily-caching compiler for `PartialSource`.
16///
17/// This would be useful in cases where:
18/// - Most partial-templates aren't used
19/// - Of the used partial-templates, they are generally used many times.
20///
21/// Note: partial-compilation error reporting is deferred to render-time so content can still be
22/// generated even when the content is in an intermediate-state.
23#[derive(Debug)]
24pub struct LazyCompiler<S: PartialSource> {
25    source: S,
26}
27
28impl<S> LazyCompiler<S>
29where
30    S: PartialSource,
31{
32    /// Create an on-demand compiler for `PartialSource`.
33    pub fn new(source: S) -> Self {
34        LazyCompiler { source }
35    }
36}
37
38impl<S> LazyCompiler<S>
39where
40    S: PartialSource + Default,
41{
42    /// Create an empty compiler for `PartialSource`.
43    pub fn empty() -> Self {
44        Default::default()
45    }
46}
47
48impl<S> Default for LazyCompiler<S>
49where
50    S: PartialSource + Default,
51{
52    fn default() -> Self {
53        Self {
54            source: Default::default(),
55        }
56    }
57}
58
59impl<S> ::std::ops::Deref for LazyCompiler<S>
60where
61    S: PartialSource,
62{
63    type Target = S;
64
65    fn deref(&self) -> &S {
66        &self.source
67    }
68}
69
70impl<S> ::std::ops::DerefMut for LazyCompiler<S>
71where
72    S: PartialSource,
73{
74    fn deref_mut(&mut self) -> &mut S {
75        &mut self.source
76    }
77}
78
79impl<S> PartialCompiler for LazyCompiler<S>
80where
81    S: PartialSource + Send + Sync + 'static,
82{
83    fn compile(self, language: sync::Arc<Language>) -> Result<Box<dyn PartialStore + Send + Sync>> {
84        let store = LazyStore {
85            language,
86            source: self.source,
87            cache: sync::Mutex::new(Default::default()),
88        };
89        Ok(Box::new(store))
90    }
91
92    fn source(&self) -> &dyn PartialSource {
93        &self.source
94    }
95}
96
97struct LazyStore<S: PartialSource> {
98    language: sync::Arc<Language>,
99    source: S,
100    cache: sync::Mutex<HashMap<String, Result<sync::Arc<dyn runtime::Renderable>>>>,
101}
102
103impl<S> LazyStore<S>
104where
105    S: PartialSource,
106{
107    fn try_get_or_create(&self, name: &str) -> Option<sync::Arc<dyn Renderable>> {
108        let mut cache = self.cache.lock().expect("not to be poisoned and reused");
109        if let Some(result) = cache.get(name) {
110            result.as_ref().ok().cloned()
111        } else {
112            let s = self.source.try_get(name)?;
113            let s = s.as_ref();
114            let template = parser::parse(s, &self.language)
115                .map(runtime::Template::new)
116                .map(sync::Arc::new)
117                .map(|t| t as sync::Arc<dyn Renderable>);
118            cache.insert(name.to_string(), template.clone());
119            template.ok()
120        }
121    }
122
123    fn get_or_create(&self, name: &str) -> Result<sync::Arc<dyn Renderable>> {
124        let mut cache = self.cache.lock().expect("not to be poisoned and reused");
125        if let Some(result) = cache.get(name) {
126            result.clone()
127        } else {
128            let s = self.source.get(name)?;
129            let s = s.as_ref();
130            let template = parser::parse(s, &self.language)
131                .map(runtime::Template::new)
132                .map(sync::Arc::new)
133                .map(|t| t as sync::Arc<dyn Renderable>);
134            cache.insert(name.to_string(), template.clone());
135            template
136        }
137    }
138}
139
140impl<S> PartialStore for LazyStore<S>
141where
142    S: PartialSource,
143{
144    fn contains(&self, name: &str) -> bool {
145        self.source.contains(name)
146    }
147
148    fn names(&self) -> Vec<&str> {
149        self.source.names()
150    }
151
152    fn try_get(&self, name: &str) -> Option<sync::Arc<dyn Renderable>> {
153        self.try_get_or_create(name)
154    }
155
156    fn get(&self, name: &str) -> Result<sync::Arc<dyn Renderable>> {
157        self.get_or_create(name)
158    }
159}
160
161impl<S> fmt::Debug for LazyStore<S>
162where
163    S: PartialSource,
164{
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        self.source.fmt(f)
167    }
168}
169
170#[cfg(test)]
171mod test {
172    use crate::partials::lazy;
173    use crate::runtime::PartialStore;
174    use crate::{partials, Language};
175    use std::{borrow, sync};
176
177    #[derive(Default, Debug, Clone, Copy)]
178    struct TestSource;
179
180    impl partials::PartialSource for TestSource {
181        fn contains(&self, _name: &str) -> bool {
182            true
183        }
184
185        fn names(&self) -> Vec<&str> {
186            vec![]
187        }
188
189        fn try_get<'a>(&'a self, name: &str) -> Option<borrow::Cow<'a, str>> {
190            match name {
191                "example.txt" => Some("Hello Liquid!".into()),
192                _ => None,
193            }
194        }
195    }
196
197    #[test]
198    fn test_store_caches_get() {
199        let options = Language::empty();
200        let store = lazy::LazyStore {
201            language: sync::Arc::new(options),
202            source: TestSource,
203            cache: sync::Mutex::new(Default::default()),
204        };
205
206        assert!(
207            !store.cache.lock().unwrap().contains_key("example.txt"),
208            "The store cache should not contain the key yet."
209        );
210
211        // Look up the partial, causing it to be cached
212        let _ = store.get("example.txt").unwrap();
213
214        assert!(
215            store.cache.lock().unwrap().contains_key("example.txt"),
216            "The store cache should now contain the key."
217        );
218    }
219
220    #[test]
221    fn test_store_caches_try_get() {
222        let options = Language::empty();
223        let store = lazy::LazyStore {
224            language: sync::Arc::new(options),
225            source: TestSource,
226            cache: sync::Mutex::new(Default::default()),
227        };
228
229        assert!(
230            !store.cache.lock().unwrap().contains_key("example.txt"),
231            "The store cache should not contain the key yet."
232        );
233
234        // Look up the partial, causing it to be cached.
235        let _ = store.try_get("example.txt").unwrap();
236
237        assert!(
238            store.cache.lock().unwrap().contains_key("example.txt"),
239            "The store cache should now contain the key."
240        );
241    }
242}