tera/
context.rs

1use std::collections::BTreeMap;
2use std::io::Write;
3
4use serde::ser::Serialize;
5use serde_json::value::{to_value, Map, Value};
6
7use crate::errors::{Error, Result as TeraResult};
8
9/// The struct that holds the context of a template rendering.
10///
11/// Light wrapper around a `BTreeMap` for easier insertions of Serializable
12/// values
13#[derive(Debug, Clone, PartialEq)]
14pub struct Context {
15    data: BTreeMap<String, Value>,
16}
17
18impl Context {
19    /// Initializes an empty context
20    pub fn new() -> Self {
21        Context { data: BTreeMap::new() }
22    }
23
24    /// Converts the `val` parameter to `Value` and insert it into the context.
25    ///
26    /// Panics if the serialization fails.
27    ///
28    /// ```rust
29    /// # use tera::Context;
30    /// let mut context = tera::Context::new();
31    /// context.insert("number_users", &42);
32    /// ```
33    pub fn insert<T: Serialize + ?Sized, S: Into<String>>(&mut self, key: S, val: &T) {
34        self.data.insert(key.into(), to_value(val).unwrap());
35    }
36
37    /// Converts the `val` parameter to `Value` and insert it into the context.
38    ///
39    /// Returns an error if the serialization fails.
40    ///
41    /// ```rust
42    /// # use tera::Context;
43    /// # struct CannotBeSerialized;
44    /// # impl serde::Serialize for CannotBeSerialized {
45    /// #     fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
46    /// #         Err(serde::ser::Error::custom("Error"))
47    /// #     }
48    /// # }
49    /// # let user = CannotBeSerialized;
50    /// let mut context = Context::new();
51    /// // user is an instance of a struct implementing `Serialize`
52    /// if let Err(_) = context.try_insert("number_users", &user) {
53    ///     // Serialization failed
54    /// }
55    /// ```
56    pub fn try_insert<T: Serialize + ?Sized, S: Into<String>>(
57        &mut self,
58        key: S,
59        val: &T,
60    ) -> TeraResult<()> {
61        self.data.insert(key.into(), to_value(val)?);
62
63        Ok(())
64    }
65
66    /// Appends the data of the `source` parameter to `self`, overwriting existing keys.
67    /// The source context will be dropped.
68    ///
69    /// ```rust
70    /// # use tera::Context;
71    /// let mut target = Context::new();
72    /// target.insert("a", &1);
73    /// target.insert("b", &2);
74    /// let mut source = Context::new();
75    /// source.insert("b", &3);
76    /// source.insert("d", &4);
77    /// target.extend(source);
78    /// ```
79    pub fn extend(&mut self, mut source: Context) {
80        self.data.append(&mut source.data);
81    }
82
83    /// Converts the context to a `serde_json::Value` consuming the context.
84    pub fn into_json(self) -> Value {
85        let mut m = Map::new();
86        for (key, value) in self.data {
87            m.insert(key, value);
88        }
89        Value::Object(m)
90    }
91
92    /// Takes a serde-json `Value` and convert it into a `Context` with no overhead/cloning.
93    pub fn from_value(obj: Value) -> TeraResult<Self> {
94        match obj {
95            Value::Object(m) => {
96                let mut data = BTreeMap::new();
97                for (key, value) in m {
98                    data.insert(key, value);
99                }
100                Ok(Context { data })
101            }
102            _ => Err(Error::msg(
103                "Creating a Context from a Value/Serialize requires it being a JSON object",
104            )),
105        }
106    }
107
108    /// Takes something that impl Serialize and create a context with it.
109    /// Meant to be used if you have a hashmap or a struct and don't want to insert values
110    /// one by one in the context.
111    pub fn from_serialize(value: impl Serialize) -> TeraResult<Self> {
112        let obj = to_value(value).map_err(Error::json)?;
113        Context::from_value(obj)
114    }
115
116    /// Returns the value at a given key index.
117    pub fn get(&self, index: &str) -> Option<&Value> {
118        self.data.get(index)
119    }
120
121    /// Remove a key from the context, returning the value at the key if the key was previously inserted into the context.
122    pub fn remove(&mut self, index: &str) -> Option<Value> {
123        self.data.remove(index)
124    }
125
126    /// Checks if a value exists at a specific index.
127    pub fn contains_key(&self, index: &str) -> bool {
128        self.data.contains_key(index)
129    }
130}
131
132impl Default for Context {
133    fn default() -> Context {
134        Context::new()
135    }
136}
137
138pub trait ValueRender {
139    fn render(&self, write: &mut impl Write) -> std::io::Result<()>;
140}
141
142// Convert serde Value to String.
143impl ValueRender for Value {
144    fn render(&self, write: &mut impl Write) -> std::io::Result<()> {
145        match *self {
146            Value::String(ref s) => write!(write, "{}", s),
147            Value::Number(ref i) => {
148                if let Some(v) = i.as_i64() {
149                    write!(write, "{}", v)
150                } else if let Some(v) = i.as_u64() {
151                    write!(write, "{}", v)
152                } else if let Some(v) = i.as_f64() {
153                    write!(write, "{}", v)
154                } else {
155                    unreachable!()
156                }
157            }
158            Value::Bool(i) => write!(write, "{}", i),
159            Value::Null => Ok(()),
160            Value::Array(ref a) => {
161                let mut first = true;
162                write!(write, "[")?;
163                for i in a.iter() {
164                    if !first {
165                        write!(write, ", ")?;
166                    }
167                    first = false;
168                    i.render(write)?;
169                }
170                write!(write, "]")?;
171                Ok(())
172            }
173            Value::Object(_) => write!(write, "[object]"),
174        }
175    }
176}
177
178pub trait ValueNumber {
179    fn to_number(&self) -> Result<f64, ()>;
180}
181// Needed for all the maths
182// Convert everything to f64, seems like a terrible idea
183impl ValueNumber for Value {
184    fn to_number(&self) -> Result<f64, ()> {
185        match *self {
186            Value::Number(ref i) => Ok(i.as_f64().unwrap()),
187            _ => Err(()),
188        }
189    }
190}
191
192// From handlebars-rust
193pub trait ValueTruthy {
194    fn is_truthy(&self) -> bool;
195}
196
197impl ValueTruthy for Value {
198    fn is_truthy(&self) -> bool {
199        match *self {
200            Value::Number(ref i) => {
201                if i.is_i64() {
202                    return i.as_i64().unwrap() != 0;
203                }
204                if i.is_u64() {
205                    return i.as_u64().unwrap() != 0;
206                }
207                let f = i.as_f64().unwrap();
208                f != 0.0 && !f.is_nan()
209            }
210            Value::Bool(ref i) => *i,
211            Value::Null => false,
212            Value::String(ref i) => !i.is_empty(),
213            Value::Array(ref i) => !i.is_empty(),
214            Value::Object(ref i) => !i.is_empty(),
215        }
216    }
217}
218
219/// Converts a dotted path to a json pointer one
220#[inline]
221#[deprecated(
222    since = "1.8.0",
223    note = "`get_json_pointer` converted a dotted pointer to a json pointer, use dotted_pointer for direct lookups of values"
224)]
225pub fn get_json_pointer(key: &str) -> String {
226    lazy_static::lazy_static! {
227        // Split the key into dot-separated segments, respecting quoted strings as single units
228        // to fix https://github.com/Keats/tera/issues/590
229        static ref JSON_POINTER_REGEX: regex::Regex = regex::Regex::new(r#""[^"]*"|[^.]+"#).unwrap();
230    }
231    let mut res = String::with_capacity(key.len() + 1);
232    if key.find('"').is_some() {
233        for mat in JSON_POINTER_REGEX.find_iter(key) {
234            res.push('/');
235            res.push_str(mat.as_str().trim_matches('"'));
236        }
237    } else {
238        res.push('/');
239        res.push_str(&key.replace('.', "/"));
240    }
241    res
242}
243
244/// following iterator immitates regex::Regex::new(r#""[^"]*"|[^.\[\]]+"#) but also strips `"` and `'`
245struct PointerMachina<'a> {
246    pointer: &'a str,
247    single_quoted: bool,
248    dual_quoted: bool,
249    escaped: bool,
250    last_position: usize,
251}
252
253impl PointerMachina<'_> {
254    fn new(pointer: &str) -> PointerMachina {
255        PointerMachina {
256            pointer,
257            single_quoted: false,
258            dual_quoted: false,
259            escaped: false,
260            last_position: 0,
261        }
262    }
263}
264
265impl<'a> Iterator for PointerMachina<'a> {
266    type Item = &'a str;
267
268    // next() is the only required method
269    fn next(&mut self) -> Option<Self::Item> {
270        let forwarded = &self.pointer[self.last_position..];
271        let mut offset: usize = 0;
272        for (i, character) in forwarded.chars().enumerate() {
273            match character {
274                '"' => {
275                    if !self.escaped {
276                        self.dual_quoted = !self.dual_quoted;
277                        if i == offset {
278                            offset += 1;
279                        } else {
280                            let result =
281                                &self.pointer[self.last_position + offset..self.last_position + i];
282
283                            self.last_position += i + 1; // +1 for skipping this quote
284                            if !result.is_empty() {
285                                return Some(result);
286                            }
287                        }
288                    }
289                }
290                '\'' => {
291                    if !self.escaped {
292                        self.single_quoted = !self.single_quoted;
293                        if i == offset {
294                            offset += 1;
295                        } else {
296                            let result =
297                                &self.pointer[self.last_position + offset..self.last_position + i];
298                            self.last_position += i + 1; // +1 for skipping this quote
299                            if !result.is_empty() {
300                                return Some(result);
301                            }
302                        }
303                    }
304                }
305                '\\' => {
306                    self.escaped = true;
307                    continue;
308                }
309                '[' => {
310                    if !self.single_quoted && !self.dual_quoted && !self.escaped {
311                        let result =
312                            &self.pointer[self.last_position + offset..self.last_position + i];
313                        self.last_position += i + 1;
314                        if !result.is_empty() {
315                            return Some(result);
316                        }
317                    }
318                }
319                ']' => {
320                    if !self.single_quoted && !self.dual_quoted && !self.escaped {
321                        offset += 1;
322                    }
323                }
324                '.' => {
325                    if !self.single_quoted && !self.dual_quoted && !self.escaped {
326                        if i == offset {
327                            offset += 1;
328                        } else {
329                            let result =
330                                &self.pointer[self.last_position + offset..self.last_position + i];
331                            self.last_position += i + 1;
332                            if !result.is_empty() {
333                                return Some(result);
334                            }
335                        }
336                    }
337                }
338                _ => (),
339            }
340            self.escaped = false;
341        }
342        if self.last_position + offset < self.pointer.len() {
343            let result = &self.pointer[self.last_position + offset..];
344            self.last_position = self.pointer.len();
345            return Some(result);
346        }
347        None
348    }
349}
350
351/// Lookups a dotted path in a json value
352/// contrary to the json slash pointer it's not allowed to begin with a dot
353#[inline]
354#[must_use]
355pub fn dotted_pointer<'a>(value: &'a Value, pointer: &str) -> Option<&'a Value> {
356    if pointer.is_empty() {
357        return Some(value);
358    }
359
360    PointerMachina::new(pointer).map(|mat| mat.replace("~1", "/").replace("~0", "~")).try_fold(
361        value,
362        |target, token| match target {
363            Value::Object(map) => map.get(&token),
364            Value::Array(list) => parse_index(&token).and_then(|x| list.get(x)),
365            _ => None,
366        },
367    )
368}
369
370/// serde jsons parse_index
371#[inline]
372fn parse_index(s: &str) -> Option<usize> {
373    if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) {
374        return None;
375    }
376    s.parse().ok()
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    use serde_json::json;
384    use std::collections::HashMap;
385
386    #[test]
387    fn test_dotted_pointer() {
388        let data = r#"{
389            "foo": {
390                "bar": {
391                    "goo": {
392                        "moo": {
393                            "cows": [
394                                {
395                                    "name": "betsy",
396                                    "age" : 2,
397                                    "temperament": "calm"
398                                },
399                                {
400                                    "name": "elsie",
401                                    "age": 3,
402                                    "temperament": "calm"
403                                },
404                                {
405                                    "name": "veal",
406                                    "age": 1,
407                                    "temperament": "ornery"
408                                }
409                            ]
410                        }
411                    }
412                },
413                "http://example.com/": {
414                    "goo": {
415                        "moo": {
416                            "cows": [
417                                {
418                                    "name": "betsy",
419                                    "age" : 2,
420                                    "temperament": "calm"
421                                },
422                                {
423                                    "name": "elsie",
424                                    "age": 3,
425                                    "temperament": "calm"
426                                },
427                                {
428                                    "name": "veal",
429                                    "age": 1,
430                                    "temperament": "ornery"
431                                }
432                            ]
433                        }
434                    }
435                }
436            }
437            }"#;
438
439        let value = serde_json::from_str(data).unwrap();
440
441        assert_eq!(dotted_pointer(&value, ""), Some(&value));
442        assert_eq!(dotted_pointer(&value, "foo"), value.pointer("/foo"));
443        assert_eq!(dotted_pointer(&value, "foo.bar.goo"), value.pointer("/foo/bar/goo"));
444        assert_eq!(dotted_pointer(&value, "skrr"), value.pointer("/skrr"));
445        assert_eq!(
446            dotted_pointer(&value, r#"foo["bar"].baz"#),
447            value.pointer(r#"/foo["bar"]/baz"#)
448        );
449        assert_eq!(
450            dotted_pointer(&value, r#"foo["bar"].baz["qux"].blub"#),
451            value.pointer(r#"/foo["bar"]/baz["qux"]/blub"#)
452        );
453    }
454
455    #[test]
456    fn can_extend_context() {
457        let mut target = Context::new();
458        target.insert("a", &1);
459        target.insert("b", &2);
460        let mut source = Context::new();
461        source.insert("b", &3);
462        source.insert("c", &4);
463        target.extend(source);
464        assert_eq!(*target.data.get("a").unwrap(), to_value(1).unwrap());
465        assert_eq!(*target.data.get("b").unwrap(), to_value(3).unwrap());
466        assert_eq!(*target.data.get("c").unwrap(), to_value(4).unwrap());
467    }
468
469    #[test]
470    fn can_create_context_from_value() {
471        let obj = json!({
472            "name": "bob",
473            "age": 25
474        });
475        let context_from_value = Context::from_value(obj).unwrap();
476        let mut context = Context::new();
477        context.insert("name", "bob");
478        context.insert("age", &25);
479        assert_eq!(context_from_value, context);
480    }
481
482    #[test]
483    fn can_create_context_from_impl_serialize() {
484        let mut map = HashMap::new();
485        map.insert("name", "bob");
486        map.insert("last_name", "something");
487        let context_from_serialize = Context::from_serialize(&map).unwrap();
488        let mut context = Context::new();
489        context.insert("name", "bob");
490        context.insert("last_name", "something");
491        assert_eq!(context_from_serialize, context);
492    }
493
494    #[test]
495    fn can_remove_a_key() {
496        let mut context = Context::new();
497        context.insert("name", "foo");
498        context.insert("bio", "Hi, I'm foo.");
499
500        let mut expected = Context::new();
501        expected.insert("name", "foo");
502        assert_eq!(context.remove("bio"), Some(to_value("Hi, I'm foo.").unwrap()));
503        assert_eq!(context.get("bio"), None);
504        assert_eq!(context, expected);
505    }
506
507    #[test]
508    fn remove_return_none_with_unknown_index() {
509        let mut context = Context::new();
510        assert_eq!(context.remove("unknown"), None);
511    }
512}