1use mlua::prelude::{Lua, LuaError, LuaFunction, LuaValue};
2
3use crate::LuaEnvironment;
4
5pub(crate) fn minijinja_types(val: &LuaValue) -> Result<&'static str, LuaError> {
9 match val {
10 LuaValue::UserData(ud) if ud.is::<LuaEnvironment>() => Ok("environment"),
11 LuaValue::UserData(ud) if ud.type_name()? == Some("state".to_string()) => Ok("state"),
12 val if val.is_null() => Ok("none"),
13 _ => Ok(val.type_name()),
14 }
15}
16
17pub(crate) fn minijinja_path_loader(lua: &Lua) -> Result<LuaFunction, LuaError> {
21 lua.load(
22 r#"
23 local function path_loader(paths)
24 if type(paths) == "string" then
25 paths = { paths }
26 end
27
28 local function loader(name)
29 if name:match("\\") then return nil end
30
31 name = name:gsub("^/+", ""):gsub("/+$", "")
32
33 local sep = package.config:sub(1,1)
34 local pattern = "([^" .. sep .. "]*)"
35
36 local splits = {}
37 for piece in name:gmatch(pattern) do
38 if ".." == piece then return nil end
39 table.insert(splits, piece)
40 end
41
42 for _, path in ipairs(paths) do
43 local p = path .. sep .. table.concat(splits, sep)
44 local file = io.open(p, "r")
45
46 if file then
47 local source = file:read("a")
48 file:close()
49
50 return source
51 end
52 end
53 end
54
55 return loader
56 end
57
58 return path_loader
59 "#,
60 )
61 .eval()
62}
63
64#[cfg(feature = "json")]
66pub mod json {
67 use minijinja::{Error as JinjaError, ErrorKind as JinjaErrorKind, State, Value as JinjaValue};
68
69 use crate::convert::err_to_minijinja_err;
70
71 pub fn add_to_environment(env: &mut minijinja::Environment) {
73 env.add_filter("fromjson", fromjson);
74 }
75
76 pub fn fromjson(_: &State, json: &[u8]) -> Result<JinjaValue, JinjaError> {
80 serde_json::from_slice(json)
81 .map_err(|err| err_to_minijinja_err(err, JinjaErrorKind::BadSerialization))
82 }
83}
84
85#[cfg(feature = "datetime")]
87pub mod datetime {
88 use jiff::civil::{Date, Time};
89 use minijinja::{
90 Error as JinjaError,
91 ErrorKind as JinjaErrorKind,
92 State,
93 Value as JinjaValue,
94 value::Kwargs,
95 };
96
97 use crate::convert::err_to_minijinja_err;
98
99 pub fn add_to_environment(env: &mut minijinja::Environment) {
101 env.add_filter("datefmt", datefmt);
102 env.add_filter("timefmt", timefmt);
103 }
104
105 pub fn datefmt(_: &State, value: JinjaValue, kwargs: Kwargs) -> Result<String, JinjaError> {
117 let format = kwargs.get::<Option<&str>>("format")?;
118 let patterns = kwargs.get::<Option<Vec<String>>>("patterns")?;
119 kwargs.assert_all_used()?;
120
121 let date = match value.as_str() {
122 Some(s) => {
123 if let Some(date) = patterns
125 .iter()
126 .flatten()
127 .find_map(|f| Date::strptime(f, s).ok())
128 {
129 Ok(date)
130 } else {
131 s.parse::<Date>()
133 .map_err(|err| err_to_minijinja_err(err, JinjaErrorKind::CannotDeserialize))
134 }
135 },
136 None => Err(JinjaError::new(
137 JinjaErrorKind::CannotDeserialize,
138 "could not parse value as a string",
139 )),
140 }?;
141
142 Ok(match format {
143 Some(f) => date.strftime(f).to_string(),
144 None => date.to_string(),
145 })
146 }
147
148 pub fn timefmt(_: &State, value: JinjaValue, kwargs: Kwargs) -> Result<String, JinjaError> {
160 let format = kwargs.get::<Option<&str>>("format")?;
161 let patterns = kwargs.get::<Option<Vec<String>>>("patterns")?;
162 kwargs.assert_all_used()?;
163
164 let time = match value.as_str() {
165 Some(s) => {
166 if let Some(date) = patterns
168 .iter()
169 .flatten()
170 .find_map(|f| Time::strptime(f, s).ok())
171 {
172 Ok(date)
173 } else {
174 s.parse::<Time>()
176 .map_err(|err| err_to_minijinja_err(err, JinjaErrorKind::CannotDeserialize))
177 }
178 },
179 None => Err(JinjaError::new(
180 JinjaErrorKind::CannotDeserialize,
181 "could not parse value as a string",
182 )),
183 }?;
184
185 Ok(match format {
186 Some(f) => time.strftime(f).to_string(),
187 None => time.to_string(),
188 })
189 }
190}
191
192#[cfg(test)]
193mod test {
194 use minijinja::context;
195 use mlua::Lua;
196 use serde_json::json;
197
198 use super::*;
199 use crate::state::LuaState;
200
201 fn setup() -> Lua {
202 Lua::new()
203 }
204
205 #[test]
206 fn test_minijinja_types_environment() {
207 let lua = setup();
208 let env = lua.create_userdata(LuaEnvironment::new()).unwrap();
209
210 assert_eq!(
211 minijinja_types(&LuaValue::UserData(env)).unwrap(),
212 "environment"
213 );
214 }
215
216 #[test]
217 fn test_minijinja_types_state() {
218 let lua = setup();
219 let env = minijinja::Environment::new();
220 let state = env.empty_state();
221
222 lua.scope(|scope| {
223 let ud = scope.create_userdata(LuaState::new(&state)).unwrap();
224 assert_eq!(minijinja_types(&LuaValue::UserData(ud)).unwrap(), "state");
225 Ok(())
226 })
227 .unwrap();
228 }
229
230 #[test]
231 fn test_minijinja_types_none() {
232 assert_eq!(minijinja_types(&LuaValue::NULL).unwrap(), "none");
233 }
234
235 #[test]
236 fn test_minijinja_types_lua() {
237 let lua = setup();
238
239 assert_eq!(minijinja_types(&LuaValue::Nil).unwrap(), "nil");
240 assert_eq!(
241 minijinja_types(&LuaValue::Boolean(true)).unwrap(),
242 "boolean"
243 );
244 assert_eq!(
245 minijinja_types(&LuaValue::Function(
246 lua.create_function(|_, _: LuaValue| Ok(())).unwrap()
247 ))
248 .unwrap(),
249 "function"
250 );
251 assert_eq!(minijinja_types(&LuaValue::Integer(99)).unwrap(), "integer");
252 assert_eq!(minijinja_types(&LuaValue::Number(99.99)).unwrap(), "number");
253 assert_eq!(
254 minijinja_types(&LuaValue::String(lua.create_string("foo").unwrap())).unwrap(),
255 "string"
256 );
257 assert_eq!(
258 minijinja_types(&LuaValue::Table(lua.create_table().unwrap())).unwrap(),
259 "table"
260 );
261 assert_eq!(
262 minijinja_types(&LuaValue::Thread(
263 lua.create_thread(lua.create_function(|_, _: LuaValue| Ok(())).unwrap())
264 .unwrap()
265 ))
266 .unwrap(),
267 "thread"
268 );
269 }
270
271 #[test]
272 #[cfg(feature = "json")]
273 fn test_minijinja_from_json_filter() {
274 let mut env = minijinja::Environment::new();
275 json::add_to_environment(&mut env);
276
277 let ex = json!({"1": 1, "2": 2, "three": [1,2,3]});
278 let expr = env.compile_expression("te | fromjson").unwrap();
279
280 let res = expr.eval(context! { te => ex.to_string() }).unwrap();
281
282 assert_eq!(res, minijinja::Value::from_serialize(ex));
283 }
284
285 #[test]
286 #[cfg(feature = "datetime")]
287 fn test_minijinja_datefmt_filter() {
288 let mut env = minijinja::Environment::new();
289 datetime::add_to_environment(&mut env);
290
291 let date = "2000-01-01";
292 let ex = "2000-01-01";
293
294 let expr = env.compile_expression("te | datefmt").unwrap();
295 let res = expr.eval(context! { te => date }).unwrap();
296
297 assert_eq!(res.as_str().unwrap(), ex, "{} should parse to {}", date, ex);
298 }
299
300 #[test]
301 #[cfg(feature = "datetime")]
302 fn test_minijinja_datefmt_filter_format() {
303 let mut env = minijinja::Environment::new();
304 datetime::add_to_environment(&mut env);
305
306 let date: &str = "2000-01-01T11:12:13";
307 let ex = "January 1, 2000";
308 let fmt = "%B %-d, %Y";
309
310 let te = format!("te | datefmt(format='{}')", fmt);
311 let expr = env.compile_expression(&te).unwrap();
312
313 let res = expr.eval(context! { te => date }).unwrap();
314
315 assert_eq!(res.as_str().unwrap(), ex, "{} should parse to {}", date, ex);
316 }
317
318 #[test]
319 #[cfg(feature = "datetime")]
320 fn test_minijinja_datefmt_filter_parse() {
321 let mut env = minijinja::Environment::new();
322 datetime::add_to_environment(&mut env);
323
324 let date = "2026 1 January";
325 let ex = "2026-01-01";
326 let patt = "%Y %-d %B";
327
328 let te = format!("te | datefmt(patterns=['{}'])", patt);
329 let expr = env.compile_expression(&te).unwrap();
330
331 let res = expr.eval(context! { te => date }).unwrap();
332
333 assert_eq!(res.as_str().unwrap(), ex, "{} should parse to {}", date, ex);
334 }
335
336 #[test]
337 #[cfg(feature = "datetime")]
338 fn test_minijinja_timefmt_filter() {
339 let mut env = minijinja::Environment::new();
340 datetime::add_to_environment(&mut env);
341
342 let time = "2000-01-01T11:12:13";
343 let ex = "11:12:13";
344
345 let expr = env.compile_expression("te | timefmt").unwrap();
346 let res = expr.eval(context! { te => time }).unwrap();
347
348 assert_eq!(res.as_str().unwrap(), ex, "{} should parse to {}", time, ex);
349 }
350
351 #[test]
352 #[cfg(feature = "datetime")]
353 fn test_minijinja_timefmt_filter_format() {
354 let mut env = minijinja::Environment::new();
355 datetime::add_to_environment(&mut env);
356
357 let time = "12:02:31";
358 let ex = "31:02:12";
359 let fmt = "%S:%M:%H";
360
361 let te = format!("te | timefmt(format='{}')", fmt);
362 let expr = env.compile_expression(&te).unwrap();
363
364 let res = expr.eval(context! { te => time }).unwrap();
365
366 assert_eq!(res.as_str().unwrap(), ex, "{} should parse to {}", time, ex);
367 }
368
369 #[test]
370 #[cfg(feature = "datetime")]
371 fn test_minijinja_timefmt_filter_parse() {
372 let mut env = minijinja::Environment::new();
373 datetime::add_to_environment(&mut env);
374
375 let time = "04 02 09";
376 let ex = "02:04:09";
377 let patt = "%M %H %S";
378
379 let te = format!("te | timefmt(patterns=['{}'])", patt);
380 let expr = env.compile_expression(&te).unwrap();
381
382 let res = expr.eval(context! { te => time }).unwrap();
383
384 assert_eq!(res.as_str().unwrap(), ex, "{} should parse to {}", time, ex);
385 }
386}