tera/builtins/
functions.rs

1use std::collections::HashMap;
2
3#[cfg(feature = "builtins")]
4use chrono::prelude::*;
5#[cfg(feature = "builtins")]
6use rand::Rng;
7use serde_json::value::{from_value, to_value, Value};
8
9use crate::errors::{Error, Result};
10
11/// The global function type definition
12pub trait Function: Sync + Send {
13    /// The global function type definition
14    fn call(&self, args: &HashMap<String, Value>) -> Result<Value>;
15
16    /// Whether the current function's output should be treated as safe, defaults to `false`
17    fn is_safe(&self) -> bool {
18        false
19    }
20}
21
22impl<F> Function for F
23where
24    F: Fn(&HashMap<String, Value>) -> Result<Value> + Sync + Send,
25{
26    fn call(&self, args: &HashMap<String, Value>) -> Result<Value> {
27        self(args)
28    }
29}
30
31pub fn range(args: &HashMap<String, Value>) -> Result<Value> {
32    let start = match args.get("start") {
33        Some(val) => match from_value::<usize>(val.clone()) {
34            Ok(v) => v,
35            Err(_) => {
36                return Err(Error::msg(format!(
37                    "Function `range` received start={} but `start` can only be a number",
38                    val
39                )));
40            }
41        },
42        None => 0,
43    };
44    let step_by = match args.get("step_by") {
45        Some(val) => match from_value::<usize>(val.clone()) {
46            Ok(v) => v,
47            Err(_) => {
48                return Err(Error::msg(format!(
49                    "Function `range` received step_by={} but `step` can only be a number",
50                    val
51                )));
52            }
53        },
54        None => 1,
55    };
56    let end = match args.get("end") {
57        Some(val) => match from_value::<usize>(val.clone()) {
58            Ok(v) => v,
59            Err(_) => {
60                return Err(Error::msg(format!(
61                    "Function `range` received end={} but `end` can only be a number",
62                    val
63                )));
64            }
65        },
66        None => {
67            return Err(Error::msg("Function `range` was called without a `end` argument"));
68        }
69    };
70
71    if start > end {
72        return Err(Error::msg(
73            "Function `range` was called with a `start` argument greater than the `end` one",
74        ));
75    }
76
77    let mut i = start;
78    let mut res = vec![];
79    while i < end {
80        res.push(i);
81        i += step_by;
82    }
83    Ok(to_value(res).unwrap())
84}
85
86#[cfg(feature = "builtins")]
87pub fn now(args: &HashMap<String, Value>) -> Result<Value> {
88    let utc = match args.get("utc") {
89        Some(val) => match from_value::<bool>(val.clone()) {
90            Ok(v) => v,
91            Err(_) => {
92                return Err(Error::msg(format!(
93                    "Function `now` received utc={} but `utc` can only be a boolean",
94                    val
95                )));
96            }
97        },
98        None => false,
99    };
100    let timestamp = match args.get("timestamp") {
101        Some(val) => match from_value::<bool>(val.clone()) {
102            Ok(v) => v,
103            Err(_) => {
104                return Err(Error::msg(format!(
105                    "Function `now` received timestamp={} but `timestamp` can only be a boolean",
106                    val
107                )));
108            }
109        },
110        None => false,
111    };
112
113    if utc {
114        let datetime = Utc::now();
115        if timestamp {
116            return Ok(to_value(datetime.timestamp()).unwrap());
117        }
118        Ok(to_value(datetime.to_rfc3339()).unwrap())
119    } else {
120        let datetime = Local::now();
121        if timestamp {
122            return Ok(to_value(datetime.timestamp()).unwrap());
123        }
124        Ok(to_value(datetime.to_rfc3339()).unwrap())
125    }
126}
127
128pub fn throw(args: &HashMap<String, Value>) -> Result<Value> {
129    match args.get("message") {
130        Some(val) => match from_value::<String>(val.clone()) {
131            Ok(v) => Err(Error::msg(v)),
132            Err(_) => Err(Error::msg(format!(
133                "Function `throw` received message={} but `message` can only be a string",
134                val
135            ))),
136        },
137        None => Err(Error::msg("Function `throw` was called without a `message` argument")),
138    }
139}
140
141#[cfg(feature = "builtins")]
142pub fn get_random(args: &HashMap<String, Value>) -> Result<Value> {
143    let start = match args.get("start") {
144        Some(val) => match from_value::<isize>(val.clone()) {
145            Ok(v) => v,
146            Err(_) => {
147                return Err(Error::msg(format!(
148                    "Function `get_random` received start={} but `start` can only be a number",
149                    val
150                )));
151            }
152        },
153        None => 0,
154    };
155
156    let end = match args.get("end") {
157        Some(val) => match from_value::<isize>(val.clone()) {
158            Ok(v) => v,
159            Err(_) => {
160                return Err(Error::msg(format!(
161                    "Function `get_random` received end={} but `end` can only be a number",
162                    val
163                )));
164            }
165        },
166        None => return Err(Error::msg("Function `get_random` didn't receive an `end` argument")),
167    };
168    let mut rng = rand::thread_rng();
169    let res = rng.gen_range(start..end);
170
171    Ok(Value::Number(res.into()))
172}
173
174pub fn get_env(args: &HashMap<String, Value>) -> Result<Value> {
175    let name = match args.get("name") {
176        Some(val) => match from_value::<String>(val.clone()) {
177            Ok(v) => v,
178            Err(_) => {
179                return Err(Error::msg(format!(
180                    "Function `get_env` received name={} but `name` can only be a string",
181                    val
182                )));
183            }
184        },
185        None => return Err(Error::msg("Function `get_env` didn't receive a `name` argument")),
186    };
187
188    match std::env::var(&name).ok() {
189        Some(res) => Ok(Value::String(res)),
190        None => match args.get("default") {
191            Some(default) => Ok(default.clone()),
192            None => Err(Error::msg(format!("Environment variable `{}` not found", &name))),
193        },
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use std::collections::HashMap;
200
201    use serde_json::value::to_value;
202
203    use super::*;
204
205    #[test]
206    fn range_default() {
207        let mut args = HashMap::new();
208        args.insert("end".to_string(), to_value(5).unwrap());
209
210        let res = range(&args).unwrap();
211        assert_eq!(res, to_value(vec![0, 1, 2, 3, 4]).unwrap());
212    }
213
214    #[test]
215    fn range_start() {
216        let mut args = HashMap::new();
217        args.insert("end".to_string(), to_value(5).unwrap());
218        args.insert("start".to_string(), to_value(1).unwrap());
219
220        let res = range(&args).unwrap();
221        assert_eq!(res, to_value(vec![1, 2, 3, 4]).unwrap());
222    }
223
224    #[test]
225    fn range_start_greater_than_end() {
226        let mut args = HashMap::new();
227        args.insert("end".to_string(), to_value(5).unwrap());
228        args.insert("start".to_string(), to_value(6).unwrap());
229
230        assert!(range(&args).is_err());
231    }
232
233    #[test]
234    fn range_step_by() {
235        let mut args = HashMap::new();
236        args.insert("end".to_string(), to_value(10).unwrap());
237        args.insert("step_by".to_string(), to_value(2).unwrap());
238
239        let res = range(&args).unwrap();
240        assert_eq!(res, to_value(vec![0, 2, 4, 6, 8]).unwrap());
241    }
242
243    #[cfg(feature = "builtins")]
244    #[test]
245    fn now_default() {
246        let args = HashMap::new();
247
248        let res = now(&args).unwrap();
249        assert!(res.is_string());
250        assert!(res.as_str().unwrap().contains('T'));
251    }
252
253    #[cfg(feature = "builtins")]
254    #[test]
255    fn now_datetime_utc() {
256        let mut args = HashMap::new();
257        args.insert("utc".to_string(), to_value(true).unwrap());
258
259        let res = now(&args).unwrap();
260        assert!(res.is_string());
261        let val = res.as_str().unwrap();
262        println!("{}", val);
263        assert!(val.contains('T'));
264        assert!(val.contains("+00:00"));
265    }
266
267    #[cfg(feature = "builtins")]
268    #[test]
269    fn now_timestamp() {
270        let mut args = HashMap::new();
271        args.insert("timestamp".to_string(), to_value(true).unwrap());
272
273        let res = now(&args).unwrap();
274        assert!(res.is_number());
275    }
276
277    #[test]
278    fn throw_errors_with_message() {
279        let mut args = HashMap::new();
280        args.insert("message".to_string(), to_value("Hello").unwrap());
281
282        let res = throw(&args);
283        assert!(res.is_err());
284        let err = res.unwrap_err();
285        assert_eq!(err.to_string(), "Hello");
286    }
287
288    #[cfg(feature = "builtins")]
289    #[test]
290    fn get_random_no_start() {
291        let mut args = HashMap::new();
292        args.insert("end".to_string(), to_value(10).unwrap());
293        let res = get_random(&args).unwrap();
294        println!("{}", res);
295        assert!(res.is_number());
296        assert!(res.as_i64().unwrap() >= 0);
297        assert!(res.as_i64().unwrap() < 10);
298    }
299
300    #[cfg(feature = "builtins")]
301    #[test]
302    fn get_random_with_start() {
303        let mut args = HashMap::new();
304        args.insert("start".to_string(), to_value(5).unwrap());
305        args.insert("end".to_string(), to_value(10).unwrap());
306        let res = get_random(&args).unwrap();
307        println!("{}", res);
308        assert!(res.is_number());
309        assert!(res.as_i64().unwrap() >= 5);
310        assert!(res.as_i64().unwrap() < 10);
311    }
312
313    #[test]
314    fn get_env_existing() {
315        std::env::set_var("TERA_TEST", "true");
316        let mut args = HashMap::new();
317        args.insert("name".to_string(), to_value("TERA_TEST").unwrap());
318        let res = get_env(&args).unwrap();
319        assert!(res.is_string());
320        assert_eq!(res.as_str().unwrap(), "true");
321        std::env::remove_var("TERA_TEST");
322    }
323
324    #[test]
325    fn get_env_non_existing_no_default() {
326        let mut args = HashMap::new();
327        args.insert("name".to_string(), to_value("UNKNOWN_VAR").unwrap());
328        let res = get_env(&args);
329        assert!(res.is_err());
330    }
331
332    #[test]
333    fn get_env_non_existing_with_default() {
334        let mut args = HashMap::new();
335        args.insert("name".to_string(), to_value("UNKNOWN_VAR").unwrap());
336        args.insert("default".to_string(), to_value("false").unwrap());
337        let res = get_env(&args).unwrap();
338        assert!(res.is_string());
339        assert_eq!(res.as_str().unwrap(), "false");
340    }
341}