/
germanubis
/
tera
Обзор
Документация
Войти
/
germanubis
/
tera
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/context.rs
512 строк
17 KB
Vincent Prouillet
Ready for v1.18 (#818)
09 мар 2023, 00:28
Не верифицирован
09 мар 2023, 00:28
226d010
Код
Авторство
О чём код?
use std::collections::BTreeMap; use std::io::Write; use serde::ser::Serialize; use serde_json::value::{to_value, Map, Value}; use crate::errors::{Error, Result as TeraResult}; /// The struct that holds the context of a template rendering. /// /// Light wrapper around a `BTreeMap` for easier insertions of Serializable /// values #[derive(Debug, Clone, PartialEq)] pub struct Context { data: BTreeMap<String, Value>, } impl Context { /// Initializes an empty context pub fn new() -> Self { Context { data: BTreeMap::new() } } /// Converts the `val` parameter to `Value` and insert it into the context. /// /// Panics if the serialization fails. /// /// ```rust /// # use tera::Context; /// let mut context = tera::Context::new(); /// context.insert("number_users", &42); /// ``` pub fn insert<T: Serialize + ?Sized, S: Into<String>>(&mut self, key: S, val: &T) { self.data.insert(key.into(), to_value(val).unwrap()); } /// Converts the `val` parameter to `Value` and insert it into the context. /// /// Returns an error if the serialization fails. /// /// ```rust /// # use tera::Context; /// # struct CannotBeSerialized; /// # impl serde::Serialize for CannotBeSerialized { /// # fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { /// # Err(serde::ser::Error::custom("Error")) /// # } /// # } /// # let user = CannotBeSerialized; /// let mut context = Context::new(); /// // user is an instance of a struct implementing `Serialize` /// if let Err(_) = context.try_insert("number_users", &user) { /// // Serialization failed /// } /// ``` pub fn try_insert<T: Serialize + ?Sized, S: Into<String>>( &mut self, key: S, val: &T, ) -> TeraResult<()> { self.data.insert(key.into(), to_value(val)?); Ok(()) } /// Appends the data of the `source` parameter to `self`, overwriting existing keys. /// The source context will be dropped. /// /// ```rust /// # use tera::Context; /// let mut target = Context::new(); /// target.insert("a", &1); /// target.insert("b", &2); /// let mut source = Context::new(); /// source.insert("b", &3); /// source.insert("d", &4); /// target.extend(source); /// ``` pub fn extend(&mut self, mut source: Context) { self.data.append(&mut source.data); } /// Converts the context to a `serde_json::Value` consuming the context. pub fn into_json(self) -> Value { let mut m = Map::new(); for (key, value) in self.data { m.insert(key, value); } Value::Object(m) } /// Takes a serde-json `Value` and convert it into a `Context` with no overhead/cloning. pub fn from_value(obj: Value) -> TeraResult<Self> { match obj { Value::Object(m) => { let mut data = BTreeMap::new(); for (key, value) in m { data.insert(key, value); } Ok(Context { data }) } _ => Err(Error::msg( "Creating a Context from a Value/Serialize requires it being a JSON object", )), } } /// Takes something that impl Serialize and create a context with it. /// Meant to be used if you have a hashmap or a struct and don't want to insert values /// one by one in the context. pub fn from_serialize(value: impl Serialize) -> TeraResult<Self> { let obj = to_value(value).map_err(Error::json)?; Context::from_value(obj) } /// Returns the value at a given key index. pub fn get(&self, index: &str) -> Option<&Value> { self.data.get(index) } /// Remove a key from the context, returning the value at the key if the key was previously inserted into the context. pub fn remove(&mut self, index: &str) -> Option<Value> { self.data.remove(index) } /// Checks if a value exists at a specific index. pub fn contains_key(&self, index: &str) -> bool { self.data.contains_key(index) } } impl Default for Context { fn default() -> Context { Context::new() } } pub trait ValueRender { fn render(&self, write: &mut impl Write) -> std::io::Result<()>; } // Convert serde Value to String. impl ValueRender for Value { fn render(&self, write: &mut impl Write) -> std::io::Result<()> { match *self { Value::String(ref s) => write!(write, "{}", s), Value::Number(ref i) => { if let Some(v) = i.as_i64() { write!(write, "{}", v) } else if let Some(v) = i.as_u64() { write!(write, "{}", v) } else if let Some(v) = i.as_f64() { write!(write, "{}", v) } else { unreachable!() } } Value::Bool(i) => write!(write, "{}", i), Value::Null => Ok(()), Value::Array(ref a) => { let mut first = true; write!(write, "[")?; for i in a.iter() { if !first { write!(write, ", ")?; } first = false; i.render(write)?; } write!(write, "]")?; Ok(()) } Value::Object(_) => write!(write, "[object]"), } } } pub trait ValueNumber { fn to_number(&self) -> Result<f64, ()>; } // Needed for all the maths // Convert everything to f64, seems like a terrible idea impl ValueNumber for Value { fn to_number(&self) -> Result<f64, ()> { match *self { Value::Number(ref i) => Ok(i.as_f64().unwrap()), _ => Err(()), } } } // From handlebars-rust pub trait ValueTruthy { fn is_truthy(&self) -> bool; } impl ValueTruthy for Value { fn is_truthy(&self) -> bool { match *self { Value::Number(ref i) => { if i.is_i64() { return i.as_i64().unwrap() != 0; } if i.is_u64() { return i.as_u64().unwrap() != 0; } let f = i.as_f64().unwrap(); f != 0.0 && !f.is_nan() } Value::Bool(ref i) => *i, Value::Null => false, Value::String(ref i) => !i.is_empty(), Value::Array(ref i) => !i.is_empty(), Value::Object(ref i) => !i.is_empty(), } } } /// Converts a dotted path to a json pointer one #[inline] #[deprecated( since = "1.8.0", note = "`get_json_pointer` converted a dotted pointer to a json pointer, use dotted_pointer for direct lookups of values" )] pub fn get_json_pointer(key: &str) -> String { lazy_static::lazy_static! { // Split the key into dot-separated segments, respecting quoted strings as single units // to fix https://github.com/Keats/tera/issues/590 static ref JSON_POINTER_REGEX: regex::Regex = regex::Regex::new(r#""[^"]*"|[^.]+"#).unwrap(); } let mut res = String::with_capacity(key.len() + 1); if key.find('"').is_some() { for mat in JSON_POINTER_REGEX.find_iter(key) { res.push('/'); res.push_str(mat.as_str().trim_matches('"')); } } else { res.push('/'); res.push_str(&key.replace('.', "/")); } res } /// following iterator immitates regex::Regex::new(r#""[^"]*"|[^.\[\]]+"#) but also strips `"` and `'` struct PointerMachina<'a> { pointer: &'a str, single_quoted: bool, dual_quoted: bool, escaped: bool, last_position: usize, } impl PointerMachina<'_> { fn new(pointer: &str) -> PointerMachina { PointerMachina { pointer, single_quoted: false, dual_quoted: false, escaped: false, last_position: 0, } } } impl<'a> Iterator for PointerMachina<'a> { type Item = &'a str; // next() is the only required method fn next(&mut self) -> Option<Self::Item> { let forwarded = &self.pointer[self.last_position..]; let mut offset: usize = 0; for (i, character) in forwarded.chars().enumerate() { match character { '"' => { if !self.escaped { self.dual_quoted = !self.dual_quoted; if i == offset { offset += 1; } else { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; // +1 for skipping this quote if !result.is_empty() { return Some(result); } } } } '\'' => { if !self.escaped { self.single_quoted = !self.single_quoted; if i == offset { offset += 1; } else { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; // +1 for skipping this quote if !result.is_empty() { return Some(result); } } } } '\\' => { self.escaped = true; continue; } '[' => { if !self.single_quoted && !self.dual_quoted && !self.escaped { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; if !result.is_empty() { return Some(result); } } } ']' => { if !self.single_quoted && !self.dual_quoted && !self.escaped { offset += 1; } } '.' => { if !self.single_quoted && !self.dual_quoted && !self.escaped { if i == offset { offset += 1; } else { let result = &self.pointer[self.last_position + offset..self.last_position + i]; self.last_position += i + 1; if !result.is_empty() { return Some(result); } } } } _ => (), } self.escaped = false; } if self.last_position + offset < self.pointer.len() { let result = &self.pointer[self.last_position + offset..]; self.last_position = self.pointer.len(); return Some(result); } None } } /// Lookups a dotted path in a json value /// contrary to the json slash pointer it's not allowed to begin with a dot #[inline] #[must_use] pub fn dotted_pointer<'a>(value: &'a Value, pointer: &str) -> Option<&'a Value> { if pointer.is_empty() { return Some(value); } PointerMachina::new(pointer).map(|mat| mat.replace("~1", "/").replace("~0", "~")).try_fold( value, |target, token| match target { Value::Object(map) => map.get(&token), Value::Array(list) => parse_index(&token).and_then(|x| list.get(x)), _ => None, }, ) } /// serde jsons parse_index #[inline] fn parse_index(s: &str) -> Option<usize> { if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) { return None; } s.parse().ok() } #[cfg(test)] mod tests { use super::*; use serde_json::json; use std::collections::HashMap; #[test] fn test_dotted_pointer() { let data = r#"{ "foo": { "bar": { "goo": { "moo": { "cows": [ { "name": "betsy", "age" : 2, "temperament": "calm" }, { "name": "elsie", "age": 3, "temperament": "calm" }, { "name": "veal", "age": 1, "temperament": "ornery" } ] } } }, "http://example.com/": { "goo": { "moo": { "cows": [ { "name": "betsy", "age" : 2, "temperament": "calm" }, { "name": "elsie", "age": 3, "temperament": "calm" }, { "name": "veal", "age": 1, "temperament": "ornery" } ] } } } } }"#; let value = serde_json::from_str(data).unwrap(); assert_eq!(dotted_pointer(&value, ""), Some(&value)); assert_eq!(dotted_pointer(&value, "foo"), value.pointer("/foo")); assert_eq!(dotted_pointer(&value, "foo.bar.goo"), value.pointer("/foo/bar/goo")); assert_eq!(dotted_pointer(&value, "skrr"), value.pointer("/skrr")); assert_eq!( dotted_pointer(&value, r#"foo["bar"].baz"#), value.pointer(r#"/foo["bar"]/baz"#) ); assert_eq!( dotted_pointer(&value, r#"foo["bar"].baz["qux"].blub"#), value.pointer(r#"/foo["bar"]/baz["qux"]/blub"#) ); } #[test] fn can_extend_context() { let mut target = Context::new(); target.insert("a", &1); target.insert("b", &2); let mut source = Context::new(); source.insert("b", &3); source.insert("c", &4); target.extend(source); assert_eq!(*target.data.get("a").unwrap(), to_value(1).unwrap()); assert_eq!(*target.data.get("b").unwrap(), to_value(3).unwrap()); assert_eq!(*target.data.get("c").unwrap(), to_value(4).unwrap()); } #[test] fn can_create_context_from_value() { let obj = json!({ "name": "bob", "age": 25 }); let context_from_value = Context::from_value(obj).unwrap(); let mut context = Context::new(); context.insert("name", "bob"); context.insert("age", &25); assert_eq!(context_from_value, context); } #[test] fn can_create_context_from_impl_serialize() { let mut map = HashMap::new(); map.insert("name", "bob"); map.insert("last_name", "something"); let context_from_serialize = Context::from_serialize(&map).unwrap(); let mut context = Context::new(); context.insert("name", "bob"); context.insert("last_name", "something"); assert_eq!(context_from_serialize, context); } #[test] fn can_remove_a_key() { let mut context = Context::new(); context.insert("name", "foo"); context.insert("bio", "Hi, I'm foo."); let mut expected = Context::new(); expected.insert("name", "foo"); assert_eq!(context.remove("bio"), Some(to_value("Hi, I'm foo.").unwrap())); assert_eq!(context.get("bio"), None); assert_eq!(context, expected); } #[test] fn remove_return_none_with_unknown_index() { let mut context = Context::new(); assert_eq!(context.remove("unknown"), None); } }