webcomposer/
lua.rs

1use crate::core::element::{Element, Render};
2pub use mlua::{IntoLua, Lua, LuaSerdeExt, Result as LuaResult, Value as LuaValue};
3use mlua::{ObjectLike, prelude::*};
4use serde::{Serialize, de::DeserializeOwned};
5use std::path::PathBuf;
6use uuid::Uuid;
7
8#[cfg(feature = "web")]
9use std::sync::{LazyLock, RwLock};
10#[cfg(feature = "web")]
11use tera::Tera;
12
13#[cfg(feature = "web")]
14static TERA: LazyLock<RwLock<Tera>> = LazyLock::new(|| RwLock::new(Tera::default()));
15
16#[macro_export]
17macro_rules! wrap_lua_runtime_err {
18    ($x:expr) => {
19        match $x {
20            Ok(x) => x,
21            Err(e) => {
22                return Err(LuaError::RuntimeError(e.to_string()));
23            }
24        }
25    };
26
27    ($x:expr, Option) => {
28        match $x {
29            Some(x) => x,
30            None => {
31                return Err(LuaError::RuntimeError("Unknown error".to_string()));
32            }
33        }
34    };
35}
36
37/// Lua VM runtime.
38pub struct LuaContext();
39
40impl LuaContext {
41    /// Implement file system stuff for the Lua vm since the built-in fs module sucks.
42    fn lua_fs(lua: &Lua) -> LuaResult<LuaTable> {
43        let v = lua.create_table()?;
44        v.set(
45            "read",
46            lua.create_function(|l, p: String| {
47                Ok(wrap_lua_runtime_err!(std::fs::read_to_string(p)).into_lua(l)?)
48            })?,
49        )?;
50        v.set(
51            "write",
52            lua.create_function(|_, (p, c): (String, String)| {
53                wrap_lua_runtime_err!(std::fs::write(p, c));
54                Ok(())
55            })?,
56        )?;
57        v.set(
58            "copy",
59            lua.create_function(|_, (f, t): (String, String)| {
60                wrap_lua_runtime_err!(std::fs::copy(f, t));
61                Ok(())
62            })?,
63        )?;
64        v.set(
65            "create_dir",
66            lua.create_function(|_, p: String| {
67                wrap_lua_runtime_err!(std::fs::create_dir(p));
68                Ok(())
69            })?,
70        )?;
71        v.set(
72            "create_dir_all",
73            lua.create_function(|_, p: String| {
74                wrap_lua_runtime_err!(std::fs::create_dir_all(p));
75                Ok(())
76            })?,
77        )?;
78        v.set(
79            "remove_file",
80            lua.create_function(|_, p: String| {
81                wrap_lua_runtime_err!(std::fs::remove_file(p));
82                Ok(())
83            })?,
84        )?;
85        v.set(
86            "remove_dir",
87            lua.create_function(|_, p: String| {
88                wrap_lua_runtime_err!(std::fs::remove_dir(p));
89                Ok(())
90            })?,
91        )?;
92        v.set(
93            "rename",
94            lua.create_function(|_, (f, t): (String, String)| {
95                wrap_lua_runtime_err!(std::fs::rename(f, t));
96                Ok(())
97            })?,
98        )?;
99        v.set(
100            "exists",
101            lua.create_function(|_, path: String| {
102                Ok(wrap_lua_runtime_err!(std::fs::exists(path)))
103            })?,
104        )?;
105        v.set(
106            "read_dir",
107            lua.create_function(|_, path: String| {
108                Ok(wrap_lua_runtime_err!(std::fs::read_dir(path))
109                    .map(|x| x.unwrap().path().to_str().unwrap().to_string())
110                    .collect::<Vec<String>>())
111            })?,
112        )?;
113        v.set(
114            "stat",
115            lua.create_function(|l, path: String| {
116                let v = l.create_table()?;
117                let stat = wrap_lua_runtime_err!(std::fs::metadata(path));
118
119                v.set("is_file", stat.is_file())?;
120                v.set("is_dir", stat.is_dir())?;
121                v.set("is_symlink", stat.is_symlink())?;
122                v.set("len", stat.len())?;
123
124                Ok(v)
125            })?,
126        )?;
127        Ok(v)
128    }
129
130    /// WebComposer Lisp Markup
131    fn lua_wclm(lua: &Lua) -> LuaResult<LuaTable> {
132        let v = lua.create_table()?;
133        v.set(
134            "parse",
135            lua.create_function(|l, v: LuaString| {
136                Ok(l.to_value(&crate::parse(&wrap_lua_runtime_err!(v.to_str())))?)
137            })?,
138        )?;
139        v.set(
140            "render",
141            lua.create_function(|l, v: LuaTable| {
142                Ok(l.from_value::<Element>(v.to_value())?.render_safe())
143            })?,
144        )?;
145        Ok(v)
146    }
147
148    /// Tera integration.
149    #[cfg(feature = "web")]
150    fn lua_tera(lua: &Lua) -> LuaResult<LuaTable> {
151        let v = lua.create_table()?;
152        v.set(
153            "add",
154            lua.create_function(|_, (name, value): (String, String)| {
155                let mut tera = TERA.write().expect("failed to lock tera state for write");
156                wrap_lua_runtime_err!(tera.add_raw_template(&name, &value));
157                Ok(())
158            })?,
159        )?;
160        v.set(
161            "render",
162            lua.create_function(|l, (tmpl, ctx): (String, LuaTable)| {
163                let tera = TERA.read().expect("failed to lock tera state for read");
164                Ok(wrap_lua_runtime_err!(tera.render(
165                    &tmpl,
166                    &wrap_lua_runtime_err!(tera::Context::from_value(
167                        l.from_value::<serde_json::Value>(ctx.to_value())?,
168                    )),
169                ))
170                .into_lua(l)?)
171            })?,
172        )?;
173        Ok(v)
174    }
175
176    /// Language features.
177    fn lua_features(lua: &Lua, args: String) -> LuaResult<LuaTable> {
178        let v = lua.create_table()?;
179        v.set("NULL", lua.null())?;
180        v.set("args", {
181            let t = lua.create_table()?;
182
183            for arg in args.split(" ") {
184                t.push(arg)?;
185            }
186
187            t
188        })?;
189        v.set("rargs", args)?;
190        v.set(
191            "match",
192            lua.create_function(|_, (value, matches): (LuaValue, LuaTable)| {
193                // search for match
194                if let Ok(x) = matches.get::<LuaValue>(&value)
195                    && !x.is_nil()
196                {
197                    if x.is_function() {
198                        return Ok(x.as_function().unwrap().call::<LuaValue>(())?);
199                    } else {
200                        return Ok(x);
201                    }
202                }
203
204                // catch-all
205                if let Ok(x) = matches.get::<LuaValue>("_")
206                    && !x.is_nil()
207                {
208                    if x.is_function() {
209                        return Ok(x.as_function().unwrap().call::<LuaValue>(())?);
210                    } else {
211                        return Ok(x);
212                    }
213                }
214
215                // error (no catch-all and no match)
216                Err(LuaError::RuntimeError(format!(
217                    "No matches available ({})",
218                    value.as_string().unwrap().to_str()?
219                )))
220            })?,
221        )?;
222        v.set(
223            "throw",
224            lua.create_function(|_, value: String| {
225                Err::<(), LuaError>(LuaError::RuntimeError(value))
226            })?,
227        )?;
228        v.set("macros", {
229            let v = lua.create_table()?;
230            v.set(
231                "define",
232                lua.create_function(|l, (name, func): (String, LuaFunction)| {
233                    l.globals().set(format!("__WEBC_MACROS_{name}"), func)?;
234                    Ok(())
235                })?,
236            )?;
237            v.set(
238                "call",
239                lua.create_async_function(async |l, (name, args): (String, LuaTable)| {
240                    let f: LuaFunction = l.globals().get(format!("__WEBC_MACROS_{name}"))?;
241                    Ok(f.call_async::<LuaTable>(args).await?)
242                })?,
243            )?;
244            v
245        })?;
246        Ok(v)
247    }
248
249    fn lua_tools(lua: &Lua) -> LuaResult<LuaTable> {
250        let v = lua.create_table()?;
251        v.set(
252            "sha256",
253            lua.create_function(|_, input: String| Ok(crate::globber::hash(input)))?,
254        )?;
255        v.set(
256            "uuid",
257            lua.create_function(|_, ()| Ok(Uuid::new_v4().to_string()))?,
258        )?;
259        v.set(
260            "uuid7",
261            lua.create_function(|_, ()| Ok(Uuid::now_v7().to_string()))?,
262        )?;
263        v.set(
264            "id",
265            lua.create_function(|_, ()| Ok(tritools::id::Id::new().printable()))?,
266        )?;
267        v.set(
268            "salt",
269            lua.create_function(|_, ()| Ok(tritools::encoding::salt()))?,
270        )?;
271        v.set(
272            "salt_len",
273            lua.create_function(|_, x: usize| Ok(tritools::encoding::salt_len(x)))?,
274        )?;
275        v.set(
276            "unix_epoch_timestamp",
277            lua.create_function(|_, ()| Ok(tritools::time::unix_epoch_timestamp()))?,
278        )?;
279        v.set(
280            "epoch_timestamp",
281            lua.create_function(|_, e: u32| Ok(tritools::time::epoch_timestamp(e)))?,
282        )?;
283        v.set(
284            "compress",
285            lua.create_function(|_, x: Vec<u8>| Ok(tritools::encoding::compress(x)))?,
286        )?;
287        v.set(
288            "decompress",
289            lua.create_function(|_, x: Vec<u8>| Ok(tritools::encoding::decompress(x)))?,
290        )?;
291        Ok(v)
292    }
293
294    fn lua_string(lua: &Lua) -> LuaResult<LuaTable> {
295        let v = lua.create_table()?;
296        v.set(
297            "replace",
298            lua.create_function(|_, (haystack, from, to): (String, String, String)| {
299                Ok(haystack.replace(&from, &to).to_string())
300            })?,
301        )?;
302        v.set(
303            "replacen",
304            lua.create_function(
305                |_, (haystack, from, to, n): (String, String, String, usize)| {
306                    Ok(haystack.replacen(&from, &to, n).to_string())
307                },
308            )?,
309        )?;
310        v.set(
311            "trim",
312            lua.create_function(|_, input: String| Ok(input.trim().to_string()))?,
313        )?;
314        v.set(
315            "trim_end",
316            lua.create_function(|_, input: String| Ok(input.trim_end().to_string()))?,
317        )?;
318        v.set(
319            "trim_start",
320            lua.create_function(|_, input: String| Ok(input.trim_start().to_string()))?,
321        )?;
322        v.set(
323            "regex_does_match",
324            lua.create_function(|_, (pat, haystack): (String, String)| {
325                let regex = regex::RegexBuilder::new(&pat)
326                    .multi_line(true)
327                    .build()
328                    .unwrap();
329
330                Ok(regex.captures(&haystack).is_some())
331            })?,
332        )?;
333        v.set(
334            "chars",
335            lua.create_function(|_, input: String| Ok(input.chars().collect::<Vec<char>>()))?,
336        )?;
337        v.set(
338            "mime",
339            lua.create_function(|_, input: String| {
340                Ok(mime_guess::from_path(input).first().unwrap().to_string())
341            })?,
342        )?;
343        v.set(
344            "mime_ext",
345            lua.create_function(|_, input: String| {
346                Ok(mime_guess::from_ext(&input).first().unwrap().to_string())
347            })?,
348        )?;
349        Ok(v)
350    }
351
352    fn lua_array(lua: &Lua) -> LuaResult<LuaTable> {
353        let v = lua.create_table()?;
354        v.set(
355            "contains",
356            lua.create_function(|_, (input, v): (LuaTable, LuaValue)| {
357                Ok(input
358                    .pairs::<LuaNumber, LuaValue>()
359                    .find(|x| v == x.as_ref().unwrap().1)
360                    .is_some())
361            })?,
362        )?;
363        v.set(
364            "push",
365            lua.create_function(|_, (input, v): (LuaTable, LuaValue)| Ok(input.push(v)))?,
366        )?;
367        v.set(
368            "pop",
369            lua.create_function(|_, input: LuaTable| Ok(input.pop::<LuaValue>()))?,
370        )?;
371        v.set(
372            "remove",
373            lua.create_function(|_, (input, k): (LuaTable, LuaValue)| Ok(input.raw_remove(k)))?,
374        )?;
375        v.set(
376            "concat",
377            lua.create_function(|_, (a, b): (LuaTable, LuaTable)| {
378                let a: Vec<(LuaValue, LuaValue)> = a.pairs().map(|x| x.unwrap()).collect();
379                let b: Vec<(LuaValue, LuaValue)> = b.pairs().map(|x| x.unwrap()).collect();
380                let c = &[a, b].concat();
381                Ok(c.iter().map(|x| x.1.to_owned()).collect::<Vec<LuaValue>>())
382            })?,
383        )?;
384        v.set(
385            "concat_table",
386            lua.create_function(|l, (a, b): (LuaTable, LuaTable)| {
387                let a: Vec<(LuaValue, LuaValue)> = a.pairs().map(|x| x.unwrap()).collect();
388                let b: Vec<(LuaValue, LuaValue)> = b.pairs().map(|x| x.unwrap()).collect();
389                let c = &[a, b].concat();
390                let d = l.create_table()?;
391
392                for (x, y) in c {
393                    d.set(x, y)?;
394                }
395
396                Ok(d)
397            })?,
398        )?;
399        Ok(v)
400    }
401
402    fn lua_memory(lua: &Lua) -> LuaResult<LuaTable> {
403        let v = lua.create_table()?;
404        v.set(
405            "clone",
406            lua.create_function(|_, v: LuaValue| Ok(v.clone()))?,
407        )?;
408        v.set(
409            "str_bytes",
410            lua.create_function(|_, v: String| Ok(v.as_bytes().to_vec()))?,
411        )?;
412        v.set(
413            "str_from_bytes",
414            lua.create_function(|_, v: Vec<u8>| Ok(String::from_utf8_lossy(&v).to_string()))?,
415        )?;
416        v.set(
417            "num_le_bytes",
418            lua.create_function(|_, v: LuaNumber| Ok(v.to_le_bytes().to_vec()))?,
419        )?;
420        v.set(
421            "num_be_bytes",
422            lua.create_function(|_, v: LuaNumber| Ok(v.to_be_bytes().to_vec()))?,
423        )?;
424        v.set("used", lua.create_function(|l, ()| Ok(l.used_memory()))?)?;
425        v.set(
426            "size_of_val",
427            lua.create_function(|_, v: LuaValue| Ok(std::mem::size_of_val(&v)))?,
428        )?;
429        v.set(
430            "compile",
431            lua.create_function(|_, v: String| {
432                let c = mlua::Compiler::new();
433                Ok(wrap_lua_runtime_err!(c.compile(v)))
434            })?,
435        )?;
436        Ok(v)
437    }
438
439    /// Create a new [`Lua`].
440    pub fn lua() -> Lua {
441        Lua::new()
442    }
443
444    /// Execute the given lua script to and capture the returned `T`.
445    pub fn lua_exec_with(
446        lua: &Lua,
447        script: Vec<u8>,
448        entry_path: String,
449        args: String,
450    ) -> LuaResult<LuaValue> {
451        let entry_path_path = PathBuf::from(entry_path.clone());
452
453        // register modules
454        lua.register_module("@webc/fs", Self::lua_fs(&lua)?)?;
455        lua.register_module("@webc/json", crate::serde::lua_json(&lua)?)?;
456        lua.register_module("@webc/toml", crate::serde::lua_toml(&lua)?)?;
457        lua.register_module("@webc/wclm", Self::lua_wclm(&lua)?)?;
458        lua.register_module("@webc/features", Self::lua_features(&lua, args)?)?;
459        lua.register_module("@webc/async", crate::tokio::lua_async(&lua)?)?;
460        lua.register_module("@webc/tools", Self::lua_tools(&lua)?)?;
461        lua.register_module("@webc/string", Self::lua_string(&lua)?)?;
462        lua.register_module("@webc/array", Self::lua_array(&lua)?)?;
463        lua.register_module("@webc/memory", Self::lua_memory(&lua)?)?;
464        lua.register_module("@webc/object", crate::object::lua_object(&lua)?)?;
465        lua.register_module("@webc/process", crate::process::lua_process(&lua)?)?;
466        #[cfg(feature = "web")]
467        lua.register_module("@webc/tera", Self::lua_tera(&lua)?)?;
468        #[cfg(feature = "web")]
469        lua.register_module("@webc/http", crate::http::lua_http(&lua)?)?;
470        #[cfg(feature = "globber")]
471        lua.register_module("@webc/globber", crate::globber::lua_globber(&lua)?)?;
472        #[cfg(feature = "db")]
473        lua.register_module("@webc/sqlite", crate::db::sqlite::lua_sqlite(&lua)?)?;
474        #[cfg(feature = "db")]
475        lua.register_module("@webc/psql", crate::db::psql::lua_psql(&lua)?)?;
476        #[cfg(feature = "db")]
477        lua.register_module("@webc/redis", crate::db::redis::lua_redis(&lua)?)?;
478
479        // add collections
480        crate::collections::array::LuaArray::create(&lua);
481        crate::collections::hashmap::LuaHashMap::create(&lua);
482
483        // execute
484        let temp = entry_path_path.canonicalize().unwrap();
485        let entry_path_path_str = temp.to_str().unwrap();
486
487        Ok(lua
488            .load(script)
489            .set_name(format!(
490                "@{}",
491                entry_path_path_str
492                    .strip_suffix(".luau")
493                    .unwrap_or(entry_path_path_str)
494            ))
495            .eval()?)
496    }
497
498    /// [`Self::lua_exec_with`], except it captures and deserializes the result.
499    pub fn lua_exec_capture_with<T>(
500        lua: &Lua,
501        script: Vec<u8>,
502        entry_path: String,
503        args: String,
504    ) -> LuaResult<T>
505    where
506        T: Serialize + DeserializeOwned,
507    {
508        lua.from_value(Self::lua_exec_with(lua, script, entry_path, args)?)
509    }
510
511    /// Execute the given lua script to and capture the returned `T`.
512    pub fn lua_exec(script: Vec<u8>, entry_path: String, args: String) -> LuaResult<LuaValue> {
513        Self::lua_exec_with(&Self::lua(), script, entry_path, args)
514    }
515
516    /// Execute the given lua script to and capture the returned `T`.
517    pub fn lua_exec_with_instance(
518        lua: &Lua,
519        script: Vec<u8>,
520        entry_path: String,
521        args: String,
522    ) -> LuaResult<LuaValue> {
523        Self::lua_exec_with(&lua, script, entry_path, args)
524    }
525
526    /// Execute the given lua script to and capture the returned `T`.
527    pub fn lua_exec_capture<T>(script: Vec<u8>, entry_path: String, args: String) -> LuaResult<T>
528    where
529        T: Serialize + DeserializeOwned,
530    {
531        Self::lua_exec_capture_with(&Self::lua(), script, entry_path, args)
532    }
533
534    /// Execute the given lua script to edit `T`.
535    pub fn lua_exec_on_with<T>(
536        lua: Lua,
537        ctx: T,
538        script: Vec<u8>,
539        entry_path: String,
540        args: String,
541    ) -> LuaResult<T>
542    where
543        T: Serialize + DeserializeOwned,
544    {
545        lua.globals().set("ctx", lua.to_value(&ctx)?)?;
546        Self::lua_exec_capture_with(&lua, script, entry_path, args)
547    }
548
549    /// Execute the given lua script to edit `T`.
550    pub fn lua_exec_on<T>(ctx: T, script: Vec<u8>, entry_path: String, args: String) -> LuaResult<T>
551    where
552        T: Serialize + DeserializeOwned,
553    {
554        let lua = Self::lua();
555        lua.globals().set("ctx", lua.to_value(&ctx)?)?;
556        Self::lua_exec_capture_with(&lua, script, entry_path, args)
557    }
558}