Current section

Files

Jump to
glua src glua.gleam
Raw

src/glua.gleam

//// A library to embed Lua in Gleam programs.
////
//// Gleam wrapper around [Luerl](https://github.com/rvirding/luerl).
import gleam/bool
import gleam/dict
import gleam/dynamic
import gleam/dynamic/decode
import gleam/int
import gleam/list
import gleam/option
import gleam/pair
import gleam/result
import gleam/string
/// Represents an instance of the Lua VM.
pub type Lua
/// Represents the errors than can happend during the parsing and execution of Lua code
pub type Error(error) {
/// The compilation process of the Lua code failed because of the presence of one or more compile errors.
LuaCompileFailure(errors: List(LuaCompileError))
/// The Lua environment threw an exception during code execution.
LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua)
/// A certain key was not found in the Lua environment.
KeyNotFound(key: List(String))
/// A Lua source file was not found
FileNotFound(path: String)
/// The value returned by the Lua environment could not be decoded using the provided decoder.
UnexpectedResultType(List(decode.DecodeError))
/// An app-defined error
CustomError(error: error)
/// An error that could not be identified.
UnknownError(error: dynamic.Dynamic)
}
/// Represents a Lua compilation error
pub type LuaCompileError {
LuaCompileError(line: Int, kind: LuaCompileErrorKind, message: String)
}
/// Represents the kind of a Lua compilation error
pub type LuaCompileErrorKind {
Parse
Tokenize
}
/// Represents the kind of exceptions that can happen at runtime during Lua code execution.
pub type LuaRuntimeExceptionKind {
/// The exception that happens when trying to access an index that does not exists on a table (also happens when indexing non-table values).
IllegalIndex(index: String, value: String)
/// The exception that happens when the `error` function is called.
ErrorCall(message: String, level: option.Option(Int))
/// The exception that happens when trying to call a function that is not defined.
UndefinedFunction(value: String)
/// The exception that happens when trying to call a method that is not defined for an object.
UndefinedMethod(object: String, method: String)
/// The exception that happens when an invalid arithmetic operation is performed.
BadArith(operator: String, args: List(String))
/// The exception that happens when a function is called with incorrect arguments.
Badarg(function: String, args: List(dynamic.Dynamic))
/// The exception that happens when a call to assert is made passing a value that evalues to `false` as the first argument.
AssertError(message: String)
/// An exception that could not be identified
UnknownException
}
/// Turns a `glua.Error` value into a human-readable string
///
/// ## Examples
///
/// ```gleam
/// let assert Error(e) = glua.run(glua.new(), glua.eval(
/// code: "if true end",
/// ))
///
/// glua.format_error(e)
/// // -> "Lua compile error: \n\nFailed to parse: error on line 1: syntax error before: 'end'"
/// ```
///
/// ```gleam
/// let assert Error(e) = glua.run(glua.new(), glua.eval(
/// code: "local a = 1; local b = true; return a + b",
/// ))
///
/// glua.format_error(e)
/// // -> "Lua runtime exception: Bad arithmetic expression: 1 + true"
/// ```
///
/// ```gleam
/// let assert Error(e) = glua.run(glua.new(), glua.get(
/// keys: ["a_value"],
/// ))
///
/// glua.format_error(e)
/// // -> "Key \"a_value\" not found"
/// ```
///
/// ```gleam
/// let assert Error(e) = glua.run(glua.new(), glua.eval_file(
/// path: "my_lua_file.lua",
/// ))
///
/// glua.format_error(e)
/// // -> "Lua source file \"my_lua_file.lua\" not found"
/// ```
///
/// ```gleam
/// let assert Error(e) = glua.run(glua.new(), {
/// use ret <- glua.then(glua.eval(
/// code: "return 1 + 1",
/// ))
/// use ref <- glua.try(list.first(ret))
///
/// glua.dereference(ref:, using: decode.string)
/// })
///
/// glua.format_error(e)
/// // -> "Expected String, but found Int"
/// ```
pub fn format_error(error: Error(e)) -> String {
case error {
LuaCompileFailure(errors) ->
"Lua compile error: "
<> "\n\n"
<> string.join(list.map(errors, format_compile_error), with: "\n")
LuaRuntimeException(exception, state) -> {
let base = "Lua runtime exception: " <> format_exception(exception)
let stacktrace = get_stacktrace(state)
case stacktrace {
"" -> base
stacktrace -> base <> "\n\n" <> stacktrace
}
}
KeyNotFound(path) ->
"Key " <> "\"" <> string.join(path, with: ".") <> "\"" <> " not found"
FileNotFound(path) ->
"Lua source file " <> "\"" <> path <> "\"" <> " not found"
UnexpectedResultType(decode_errors) ->
list.map(decode_errors, format_decode_error) |> string.join(with: "\n")
CustomError(error) -> string.inspect(error)
UnknownError(error) -> "Unknown error: " <> format_unknown_error(error)
}
}
fn format_compile_error(error: LuaCompileError) -> String {
let kind = case error.kind {
Parse -> "parse"
Tokenize -> "tokenize"
}
"Failed to "
<> kind
<> ": error on line "
<> int.to_string(error.line)
<> ": "
<> error.message
}
fn format_exception(exception: LuaRuntimeExceptionKind) -> String {
case exception {
IllegalIndex(index, value) ->
"Invalid index "
<> "\""
<> index
<> "\""
<> " at object "
<> "\""
<> value
<> "\""
ErrorCall(msg, level) -> {
let base = "Error call: " <> msg
case level {
option.Some(level) -> base <> " at level " <> int.to_string(level)
option.None -> base
}
}
UndefinedFunction(fun) -> "Undefined function: " <> fun
UndefinedMethod(obj, method) ->
"Undefined method "
<> "\""
<> method
<> "\""
<> " for object: "
<> "\""
<> obj
<> "\""
BadArith(operator, args) ->
"Bad arithmetic expression: "
<> string.join(args, with: " " <> operator <> " ")
Badarg(function, args) ->
"Bad argument "
<> string.join(list.map(args, format_lua_value), with: ", ")
<> " for function "
<> function
AssertError(msg) -> "Assertion failed with message: " <> msg
UnknownException -> "Unknown exception"
}
}
@external(erlang, "glua_ffi", "get_stacktrace")
fn get_stacktrace(state: Lua) -> String
fn format_decode_error(error: decode.DecodeError) -> String {
let base = "Expected " <> error.expected <> ", but found " <> error.found
case error.path {
[] -> base
path -> base <> " at " <> string.join(path, with: ".")
}
}
@external(erlang, "luerl_lib", "format_value")
fn format_lua_value(v: anything) -> String
@external(erlang, "luerl_lib", "format_error")
fn format_unknown_error(error: dynamic.Dynamic) -> String
/// Represents an action that can be run within a Lua state and potentially mutates that state.
///
/// `Action`s are how we interact with a Lua VM and thus many functions in this library
/// returns an `Action` when invoked. It is important to note that the execution of any `Action` is defered until
/// you pass it to `glua.run`:
///
/// ```gleam
/// // no Lua code has been evaluated or even parsed,
/// // we're just creating an `Action`
/// let action = glua.eval("return 1")
///
/// glua.run(glua.new(), action) // now our Lua code is evaluated
/// ```
///
/// `Action`s *can* fail and *can* mutate the Lua state. When calling multiple `Action`s in sequence,
/// you need to make sure each one is executed within the Lua state returned by the previous one since
/// executing an `Action` using outdated state could lead to unexpected behaviour.
///
/// ```gleam
/// let state = glua.new()
/// let result = {
/// use #(new_state, _) <- result.try(
/// glua.exec(state, glua.set(keys: ["a_number"], value: glua.int(36)))
/// )
/// use #(new_state, ret) <- result.try(
/// glua.exec(state, glua.eval("return math.sqrt(a_number)"))
/// )
///
/// // we know that `math.sqrt` only returns one value
/// let assert [ref] = ret
///
/// glua.run(new_state, glua.dereference(ref:, using: decode.float))
/// }
/// result
/// // -> Ok(6.0)
/// ```
///
/// However, `glua` provides function to compose `Actions`s toghether without having to pass
/// the state explicitly. The most common of such functions is `glua.then`, which allows us to take
/// an existing `Action` and use its return value to construct another `Action`.
/// `glua.then` will automatically pass the state returned by the first action to the second one
/// and it will halt the chain as soon as any `Action` fails (like `result.try`).
/// This is equivalent to the above example:
///
/// ```gleam
/// let state = glua.new()
/// let action = {
/// use _ <- glua.then(glua.set(keys: ["a_number"], value: glua.int(36)))
/// use ret <- glua.then(glua.eval(code: "return math.sqrt(a_number)"))
///
/// // we know that `math.sqrt` only returns one value
/// let assert [ref] = ret
/// glua.dereference(ref:, using: decode.float)
/// }
///
/// glua.run(state, action)
/// // -> Ok(6.0)
/// ```
///
/// An `Action` takes two types parameters, `return` is the type of the value that the `Action`
/// would return in case it succeeds, and `error` is the type of custom errors that
/// the `Action` could return.
pub opaque type Action(return, error) {
Action(function: fn(Lua) -> Result(#(Lua, return), Error(error)))
}
/// Runs an `Action` within a Lua environment.
///
/// ## Examples
///
/// ```gleam
/// let state = glua.new()
///
/// glua.eval(code: "return 'Hello from Lua!'")
/// |> glua.returning_multi(using: decode.string)
/// |> glua.run(state, _)
/// // -> Ok("Hello from Lua!")
/// ```
pub fn run(
state lua: Lua,
action action: Action(return, error),
) -> Result(return, Error(error)) {
exec(lua, action) |> result.map(pair.second)
}
/// Runs an `Action` within a Lua environment and returns both the result
/// and the updated Lua state in case of no errors.
///
/// ## Examples
///
/// ```gleam
/// let state = glua.new()
/// let assert Ok(#(new_state, Nil)) =
/// glua.exec(state, glua.set(["my_value"], glua.string("Hello!")))
///
/// glua.exec(
/// state:,
/// action: glua.eval(code: "return my_value") |> glua.returning_multi(decode.string)
/// )
///
/// // -> Ok(#(_state, ["Hello!"]))
/// ```
pub fn exec(
state lua: Lua,
action action: Action(return, error),
) -> Result(#(Lua, return), Error(error)) {
action.function(lua)
}
/// Composes two `Action`s into a single one, by executing the first one and passing its return value
/// to a function that returns another `Action`.
///
/// If the first `Action` returns an `Error` when executed, then the function is not called
/// and the error is returned.
///
/// This function is the most common way to chain together multiple `Action`s.
///
/// ## Examples
///
/// ```gleam
/// let my_value = 1
/// let assert Ok(#(_state, ret)) = glua.run(glua.new(), {
/// use _ <- glua.then(glua.set(keys: ["my_value"], value: glua.int(my_value)))
/// glua.get(keys: ["my_value"]) |> glua.returning(decode.int)
/// })
///
/// assert ret == my_value
/// ```
///
/// ```gleam
/// glua.run(glua.new(), {
/// use ret <- glua.then(glua.eval_file(path: "./my_file.lua"))
/// glua.call_function_by_name(path: ["table", "pack"], args: ret)
/// })
/// // -> Error(glua.FileNotFound("./my_file.lua"))
/// ```
pub fn then(action: Action(a, e), next: fn(a) -> Action(b, e)) -> Action(b, e) {
use state <- Action
use #(new, ret) <- result.try(action.function(state))
next(ret).function(new)
}
/// Tries to update the return value of an `Action` by passing it to a function
/// that yields a result.
///
/// This is a shorthand for writing a case with `glua.then`:
///
/// ```gleam
/// use fun <- glua.then(glua.get(["string", "reverse"]))
/// glua.call_function(fun:, args: [glua.string("Hello")])
/// |> glua.try(list.first)
/// |> glua.returning(decode.string)
/// ```
///
/// as opposed to this:
///
/// ```gleam
/// use fun <- glua.then(glua.get(["string", "reverse"]))
/// use return <- glua.then(glua.call_function(fun, [glua.string("Hello")]))
/// case return {
/// [first] -> glua.dereference(ref: first, using: decode.string)
/// _ -> glua.failure(Nil)
/// }
/// ```
pub fn try(
action action: Action(a, e),
apply fun: fn(a) -> Result(b, e),
) -> Action(b, e) {
use ret <- then(action)
case fun(ret) {
Ok(x) -> success(x)
Error(e) -> failure(e)
}
}
/// Runs a callback function if the given bool is `False`, otherwise return a failing `Action`
/// using the provided value.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), {
/// use ret <- glua.then(glua.eval(code: "local a = 1"))
/// use <- glua.guard(when: ret == [], return: "expected at least one value from Lua")
///
/// glua.fold(ret, glua.dereference(_, using: decode.int))
/// })
/// // -> Error(glua.CustomError("expected at least one value from Lua"))
/// ```
pub fn guard(
when requirement: Bool,
return consequence: e,
otherwise alternative: fn() -> Action(a, e),
) -> Action(a, e) {
bool.guard(requirement, failure(consequence), alternative)
}
/// Creates an `Action` that always succeeds and returns `value`.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), glua.success("my value"))
/// // -> Ok("my_value")
/// ```
pub fn success(value: a) -> Action(a, e) {
use state <- Action
Ok(#(state, value))
}
/// Creates an `Action` that always fails with `glua.CustomError(error)`.
///
/// ## Examples
///
/// ```gleam
/// glua.run(
/// glua.new(),
/// glua.failure("incorrect number of return values")
/// )
/// // -> Error(glua.CustomError("incorrect number of return values"))
/// ```
pub fn failure(error: e) -> Action(a, e) {
use _ <- Action
Error(CustomError(error))
}
/// Returns the `error` function.
@external(erlang, "glua_stdlib_ffi", "error")
fn error_func() -> Value
const non_error = "The error function returned a non error which should never happen! Please report this issue to the git repository."
/// Invokes the Lua `error` function with the provided message.
pub fn error(message: String) -> Action(a, e) {
use state <- Action
case call_function(error_func(), [string(message)]).function(state) {
Ok(_) -> panic as non_error
Error(e) -> Error(e)
}
}
/// Invokes the Lua `error` function with the provided message and level.
pub fn error_with_level(message: String, level: Int) -> Action(a, e) {
use state <- Action
case
call_function(error_func(), [string(message), int(level)]).function(state)
{
Ok(_) -> panic as non_error
Error(e) -> Error(e)
}
}
/// Transforms the return value of an `Action` with the provided function.
///
/// If the `Action` returns an `Error` when executed then the function is not called and the
/// error is returned.
///
/// ## Examples
///
/// ```gleam
/// glua.get(keys: ["_VERSION"])
/// |> glua.returning(using: decode.string)
/// |> glua.map(fn(version) {
/// "glua supports " <> version
/// })
/// |> glua.run(glua.new(), _)
/// // -> Ok("glua supports Lua 5.3")
/// ```
///
/// ```gleam
/// glua.run(glua.new(), {
/// use n <- glua.map(glua.get(keys: ["my_number"]))
/// n * 2
/// })
/// // -> Error(glua.KeyNotFound(["my_number"]))
/// ```
pub fn map(over action: Action(a, e), with fun: fn(a) -> b) -> Action(b, e) {
use state <- Action
action.function(state)
|> result.map(pair.map_second(_, fun))
}
/// Maps a list of elements into a list of `Action`s by calling a function in each element and then flattens
/// all the `Action`s into a single one.
///
/// ## Examples
///
/// ```gleam
/// let numbers = [9, 16, 25]
/// let keys = ["math", "sqrt"]
/// glua.run(glua.new(), glua.fold(numbers, fn(n) {
/// glua.call_function_by_name(keys:, args: [glua.int(n)])
/// |> glua.try(list.first)
/// |> glua.returning(using: decode.float)
/// }))
/// // -> Ok([3.0, 4.0, 5.0])
/// ```
pub fn fold(
over list: List(a),
with fun: fn(a) -> Action(b, e),
) -> Action(List(b), e) {
use state <- Action
list.try_fold(list, #(state, []), fn(acc, e) {
let #(state, results) = acc
fun(e).function(state)
|> result.map(pair.map_second(_, fn(ret) { [ret, ..results] }))
})
|> result.map(pair.map_second(_, list.reverse))
}
/// Represents a chunk of Lua code that is already loaded into the Lua VM
pub type Chunk
/// Represents a value that can be passed to the Lua environment.
pub type Value
@external(erlang, "glua_ffi", "coerce_nil")
pub fn nil() -> Value
@external(erlang, "glua_ffi", "coerce")
pub fn string(v: String) -> Value
@external(erlang, "glua_ffi", "coerce")
pub fn bool(v: Bool) -> Value
@external(erlang, "glua_ffi", "coerce")
pub fn int(v: Int) -> Value
@external(erlang, "glua_ffi", "coerce")
pub fn float(v: Float) -> Value
pub fn table(values: List(#(Value, Value))) -> Action(Value, e) {
use state <- Action
Ok(do_table(values, state) |> pair.swap)
}
pub fn table_list(values: List(Value)) -> Action(Value, e) {
let #(_idx, values) =
list.map_fold(values, 1, fn(acc, val) { #(acc + 1, #(int(acc), val)) })
table(values)
}
@external(erlang, "luerl_heap", "alloc_table")
fn do_table(values: List(#(Value, Value)), lua: Lua) -> #(Value, Lua)
/// A decoder for list-style Lua tables.
/// This decoder works similarly to ipairs in the sense that it stops
/// when there is a gap in the table list.
///
/// ## Examples
///
/// ```gleam
/// glua.eval("return { 1, 2, 3 }")
/// |> glua.try(list.first)
/// |> glua.returning(glua.table_list_decoder(decode.int))
/// |> glua.run(glua.new(), _)
/// // -> Ok([1, 2, 3])
/// ```
///
/// ```gleam
/// glua.eval("return { [1] = 'a', [2] = 'b', [4] = 'd'}")
/// |> glua.try(list.first)
/// |> glua.returning(glua.table_list_decoder(decode.string))
/// |> glua.run(glua.new(), _)
/// // -> Ok(["a", "b"])
/// ```
pub fn table_list_decoder(
inner decoder: decode.Decoder(a),
) -> decode.Decoder(List(a)) {
decode.dict(decode.int, decoder) |> decode.map(list_loop(_, [], 1))
}
fn list_loop(dict: dict.Dict(Int, a), acc: List(a), idx: Int) {
case dict.get(dict, idx) {
Ok(it) -> list_loop(dict, [it, ..acc], idx + 1)
Error(Nil) -> list.reverse(acc)
}
}
/// Encodes a Gleam function into a Lua function.
///
/// > **Note**: The function to be encoded has to return an `Action` with a `Never` type
/// > as the `error` parameter, meaning that the function cannot invoke `glua.failure` in its body.
/// > If you want to return an error inside that function, you should use `glua.error` or `glua.error_with_code`,
/// > both of which will call the Lua `error` function.
pub fn function(f: fn(List(Value)) -> Action(List(Value), Never)) -> Value {
do_function(f)
}
// Taken from hexdocs.pm/funtil/1.1.0/funtil.html#Never
/// This type is used to represent a value that can never happen. What does that
/// mean exactly?
///
/// - A `Bool` is a type that has two values: `True` and `False`.
/// - `Nil` is a type that has one value: `Nil`.
/// - `Never` is a type that has zero values: it's impossible to construct!
///
/// This library uses this type to make `glua.failure` impossible to construct in `glua.function`s
/// to encourage using `glua.error` instead since `glua.failure` wouldn't make sense in that case.
pub type Never
pub fn function_decoder() -> decode.Decoder(
fn(List(Value)) -> Action(List(Value), e),
) {
decode.new_primitive_decoder("LuaFunction", decode_lua_function)
}
@external(erlang, "glua_ffi", "decode_fun")
fn decode_lua_function(
v: dynamic.Dynamic,
) -> Result(
fn(List(Value)) -> Action(List(Value), e),
fn(List(Value)) -> Action(List(Value), e),
)
/// Encodes any Gleam value as a reference that can be passed to a Lua program.
///
/// Deferencing a userdata value inside Lua code will cause a Lua exception.
///
/// ## Examples
///
/// ```gleam
/// pub type User {
/// User(name: String, is_admin: Bool)
/// }
///
/// let user_decoder = {
/// use name <- decode.field(1, decode.string)
/// use is_admin <- decode.field(2, decode.bool)
/// decode.success(User(name:, is_admin:))
/// }
///
/// glua.run(glua.new(), {
/// use userdata <- glua.then(userdata(User("Jhon Doe", False)))
/// use _ <- glua.then(glua.set(
/// keys: ["a_user"],
/// value: userdata
/// ))
///
/// glua.eval(code: "return a_user")
/// |> glua.try(list.first)
/// |> glua.returning(using: user_decoder)
/// })
/// // -> Ok(User("Jhon Doe", False))
/// ```
///
/// ```gleam
/// pub type Person {
/// Person(name: String, email: String)
/// }
///
/// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_), _)) =
/// glua.run(glua.new(), {
/// use userdata <- glua.then(glua.userdata(
/// Person(name: "Lucy", email: "lucy@example.com")
/// ))
/// use _ <- glua.then(glua.set(
/// keys: ["lucy"],
/// value: userdata
/// ))
///
/// glua.eval(code: "return lucy.email")
/// })
/// ```
pub fn userdata(v: anything) -> Action(Value, e) {
use state <- Action
Ok(do_userdata(v, state) |> pair.swap)
}
@external(erlang, "luerl_heap", "alloc_userdata")
fn do_userdata(v: anything, lua: Lua) -> #(Value, Lua)
@external(erlang, "glua_ffi", "wrap_fun")
fn do_function(fun: fn(List(Value)) -> Action(List(Value), e)) -> Value
/// Converts a reference to a Lua value into type-safe Gleam data using the provided decoder.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), {
/// use ref <- glua.then(
/// glua.eval(code: "return 'Hello from Lua!'")
/// |> glua.try(list.first)
/// )
///
/// glua.dereference(ref:, using: decode.string)
/// })
/// // -> Ok("Hello from Lua!")
/// ```
///
/// ```gleam
/// let assert Ok(#(state, [ref1, ref2])) = glua.exec(
/// glua.new(),
/// glua.eval(code: "return 1, true")
/// )
///
/// let assert Ok(1) =
/// glua.run(state, glua.dereference(ref: ref1, using: decode.int))
/// let assert Ok(True) =
/// glua.run(state, glua.dereference(ref: ref2, using: decode.bool))
/// ```
pub fn dereference(
ref ref: Value,
using decoder: decode.Decoder(a),
) -> Action(a, e) {
use state <- Action
use ret <- result.map(
do_dereference(state, ref)
|> decode.run(decoder)
|> result.map_error(UnexpectedResultType),
)
#(state, ret)
}
@external(erlang, "glua_ffi", "dereference")
fn do_dereference(lua: Lua, ref: Value) -> dynamic.Dynamic
/// Transforms an `Action` that returns a reference to a Lua value into an `Action` that returns
/// a typed Gleam value.
///
/// ## Examples
///
/// ```gleam
/// let decoder =
/// decode.dict(decode.string, decode.int)
/// |> decode.map(dict.to_list)
///
/// glua.eval(code: "return { a = 1, b = 2 }")
/// |> glua.try(apply: list.first)
/// |> glua.returning(using: decoder)
/// |> glua.run(glua.new(), _)
///
/// // -> Ok([#("a", 1), #("b", 2)])
/// ```
pub fn returning(
action act: Action(Value, e),
using decoder: decode.Decoder(a),
) -> Action(a, e) {
use ref <- then(act)
dereference(ref, decoder)
}
/// Same as `glua.returning`, but works on an `Action` that returns multiple references to Lua values
/// instead of a single one.
pub fn returning_multi(
action act: Action(List(Value), e),
using decoder: decode.Decoder(a),
) -> Action(List(a), e) {
use refs <- then(act)
fold(refs, dereference(_, decoder))
}
/// Creates a new Lua VM instance
@external(erlang, "luerl", "init")
pub fn new() -> Lua
/// List of Lua modules and functions that will be sandboxed by default
pub const default_sandbox = [
["io"],
["file"],
["os", "execute"],
["os", "exit"],
["os", "getenv"],
["os", "remove"],
["os", "rename"],
["os", "tmpname"],
["package"],
["load"],
["loadfile"],
["require"],
["dofile"],
["loadstring"],
]
/// Creates a new Lua VM instance with sensible modules and functions sandboxed.
///
/// Check `glua.default_sandbox` to see what modules and functions will be sandboxed.
///
/// This function accepts a list of paths to Lua values that will be excluded from being sandboxed,
/// so needed modules or functions can be enabled while keeping sandboxed the rest.
/// In case you want to sandbox more Lua values, pass to `glua.sandbox` the returned Lua state.
pub fn new_sandboxed(
allow excluded: List(List(String)),
) -> Result(Lua, Error(e)) {
list_substraction(default_sandbox, excluded)
|> list.try_fold(from: new(), with: sandbox)
}
@external(erlang, "erlang", "--")
fn list_substraction(a: List(a), b: List(a)) -> List(a)
/// Swaps out the value at `keys` with a function that causes a Lua error when called.
///
/// ## Examples
///
/// ```gleam
/// let assert Ok(state) = glua.new() |> glua.sandbox(["os"], ["execute"])
/// let assert Error(glua.LuaRuntimeException(exception, _)) =
/// glua.run(state, glua.eval(
/// code: "os.execute(\"rm -f important_file\"); return 0",
/// ))
///
/// // 'important_file' was not deleted
/// assert exception == glua.ErrorCall(["os.execute is sandboxed"])
/// ```
pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, Error(e)) {
let msg = string.join(keys, with: ".") <> " is sandboxed"
set(["_G", ..keys], sandbox_fun(msg)).function(lua)
|> result.map(pair.first)
}
@external(erlang, "glua_ffi", "sandbox_fun")
fn sandbox_fun(msg: String) -> Value
/// Gets a value in the Lua environment.
///
/// ## Examples
///
/// ```gleam
/// glua.get(keys: ["_VERSION"])
/// |> glua.returning(using: decode.string)
/// |> glua.run(glua.new(), _)
/// // -> Ok("Lua 5.3")
/// ```
///
/// ```gleam
/// glua.run(glua.new(), {
/// use _ <- glua.then(glua.set(
/// keys: ["my_table", "my_value"],
/// value: glua.bool(True)
/// ))
///
/// glua.get(keys: ["my_table", "my_value"]))
/// |> glua.returning(using: decode.bool)
/// })
/// // -> Ok(True)
/// ```
///
/// ```gleam
/// glua.run(glua.new(), glua.get(keys: ["non_existent"]))
/// // -> Error(glua.KeyNotFound(["non_existent"]))
/// ```
pub fn get(keys keys: List(String)) -> Action(Value, e) {
use state <- Action
use ret <- result.map(do_get(state, keys))
#(state, ret)
}
@external(erlang, "glua_ffi", "get_table_keys")
fn do_get(lua: Lua, keys: List(String)) -> Result(Value, Error(e))
/// Gets a private value that is not exposed to the Lua runtime.
///
/// ## Examples
///
/// ```gleam
/// assert glua.new()
/// |> glua.set_private("private_value", "secret_value")
/// |> glua.get_private("private_value", decode.string)
/// == Ok("secret_value")
/// ```
pub fn get_private(
state lua: Lua,
key key: String,
using decoder: decode.Decoder(a),
) -> Result(a, Error(e)) {
use value <- result.try(do_get_private(lua, key))
decode.run(value, decoder) |> result.map_error(UnexpectedResultType)
}
@external(erlang, "glua_ffi", "get_private")
fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, Error(e))
/// Sets a value in the Lua environment.
///
/// All nested keys will be created as intermediate tables.
///
/// If successfull, this function will return the updated Lua state
/// and the setted value will be available in Lua scripts.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), {
/// use _ <- glua.then(glua.set(
/// keys: ["my_number"],
/// value: glua.int(10)
/// ))
///
/// glua.get(keys: ["my_number"])
/// |> glua.returning(using: decode.int)
/// })
/// // -> Ok(10)
/// ```
///
/// ```gleam
/// let emails = ["jhondoe@example.com", "lucy@example.com"]
/// let assert Ok(results) = glua.run(glua.new(), {
/// use encoded <- glua.then(glua.table(
/// list.index_map(emails, fn(email, i) { #(glua.int(i + 1), glua.string(email)) })
/// ))
/// use _ <- glua.then(glua.set(["info", "emails"], encoded))
///
/// glua.eval(code: "return info.emails"))
/// |> glua.try(list.first)
/// |> glua.returning(using: glua.table_list_decoder(decode.string))
/// })
///
/// assert results == emails
/// ```
pub fn set(keys keys: List(String), value val: Value) -> Action(Nil, e) {
use state <- Action
use #(new, keys) <- result.try(
list.try_fold(keys, #(state, []), fn(acc, key) {
let #(state, keys) = acc
let keys = list.append(keys, [key])
case do_get(state, keys) {
Ok(_) -> Ok(#(state, keys))
Error(KeyNotFound(_)) -> {
let #(tbl, new) = do_table([], state)
use new <- result.map(do_set(new, keys, tbl))
#(new, keys)
}
Error(e) -> Error(e)
}
}),
)
do_set(new, keys, val)
|> result.map(fn(state) { #(state, Nil) })
}
/// Sets a value that is not exposed to the Lua runtime and can only be accessed from Gleam.
///
/// ## Examples
/// ```gleam
/// assert glua.new()
/// |> glua.set("secret_value", "private_value")
/// |> glua.get("secret_value", decode.string)
/// == Ok("secret_value")
/// ```
pub fn set_private(state lua: Lua, key key: String, value value: a) -> Lua {
do_set_private(key, value, lua)
}
/// Sets a group of values under a particular table in the Lua environment.
pub fn set_api(
keys: List(String),
values: List(#(String, Value)),
) -> Action(Nil, e) {
use _ <- then(
fold(values, fn(pair) { set(list.append(keys, [pair.0]), pair.1) }),
)
success(Nil)
}
/// Sets the paths where the Lua runtime will look when requiring other Lua files.
///
/// > **Warning**: This function will not work properly if `["package"]` or `["require"]` are sandboxed
/// > in the provided Lua state. If you constructed the Lua state using `glua.new_sandboxed`,
/// > remember to allow the required values by passing `[["package"], ["require"]]` to `glua.new_sandboxed`.
///
/// ## Examples
///
/// ```gleam
/// let my_scripts_paths = ["app/scripts/lua/?.lua"]
/// glua.run(glua.new(), {
/// use _ <- glua.then(glua.set_lua_paths(paths: my_scripts_paths))
///
/// glua.eval(
/// code: "local my_math = require 'my_script'; return my_math.square(3)"
/// )
/// |> glua.try(list.first)
/// |> glua.returning(decode.int)
/// })
/// // -> Ok(9)
/// ```
pub fn set_lua_paths(paths paths: List(String)) -> Action(Nil, e) {
let paths = string.join(paths, with: ";") |> string
set(["package", "path"], paths)
}
@external(erlang, "glua_ffi", "set_table_keys")
fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, Error(e))
@external(erlang, "luerl", "put_private")
fn do_set_private(key: String, value: a, lua: Lua) -> Lua
/// Remove a private value that is not exposed to the Lua runtime.
///
/// ## Examples
///
/// ```gleam
/// let lua = glua.set_private(glua.new(), "my_value", "will_be_removed")
/// assert glua.get(lua, "my_value", decode.string) == Ok("will_be_removed")
///
/// assert glua.delete_private(lua, "my_value")
/// |> glua.get("my_value", decode.string)
/// == Error(glua.KeyNotFound(["my_value"]))
/// ```
pub fn delete_private(state lua: Lua, key key: String) -> Lua {
do_delete_private(key, lua)
}
@external(erlang, "luerl", "delete_private")
fn do_delete_private(key: String, lua: Lua) -> Lua
/// Parses a string of Lua code and returns it as a compiled chunk.
///
/// To eval the returned chunk, use `glua.eval_chunk`.
pub fn load(code code: String) -> Action(Chunk, e) {
Action(do_load(_, code))
}
@external(erlang, "glua_ffi", "load")
fn do_load(lua: Lua, code: String) -> Result(#(Lua, Chunk), Error(e))
/// Parses a Lua source file and returns it as a compiled chunk.
///
/// To eval the returned chunk, use `glua.eval_chunk`.
pub fn load_file(path path: String) -> Action(Chunk, e) {
Action(do_load_file(_, path))
}
@external(erlang, "glua_ffi", "load_file")
fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), Error(e))
/// Evaluates a string of Lua code.
///
/// ## Examples
///
/// ```gleam
/// glua.eval(code: "return 1 + 2")
/// |> glua.returning_multi(using: decode.int)
/// |> glua.run(glua.new(), _)
/// // -> Ok([3])
/// ```
///
/// ```gleam
/// let assert Ok(#(state, [ref1, ref2])) = glua.exec(glua.new(), glua.eval(
/// code: "return 'hello, world!', 10",
/// ))
///
/// let assert Ok("hello world") =
/// glua.run(state, glua.dereference(ref: ref1, using: decode.string))
/// let assert Ok(10) =
/// glua.run(state, glua.dereference(ref: ref2, using: decode.int))
/// ```
///
/// ```gleam
/// glua.run(glua.new(), glua.eval(code: "return 1 * "))
/// // -> Error(glua.LuaCompileFailure(
/// [glua.LuaCompileError(1, Parse, "syntax error before: ")]
/// ))
/// ```
///
/// > **Note**: If you are evaluating the same piece of code multiple times,
/// > instead of calling `glua.eval` repeatly it is recommended to first convert
/// > the code to a chunk by passing it to `glua.load`, and then
/// > evaluate that chunk using `glua.eval_chunk`.
pub fn eval(code code: String) -> Action(List(Value), e) {
Action(do_eval(_, code))
}
@external(erlang, "glua_ffi", "eval")
fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), Error(e))
/// Evaluates a compiled chunk of Lua code.
///
/// ## Examples
///
/// ```gleam
/// glua.load(code: "return 'hello, world!'")
/// |> glua.then(glua.eval_chunk)
/// |> glua.returning_multi(using: decode.string)
/// |> glua.run(glua.new(), _)
/// // -> Ok(["hello, world!"])
/// ```
pub fn eval_chunk(chunk chunk: Chunk) -> Action(List(Value), e) {
Action(do_eval_chunk(_, chunk))
}
@external(erlang, "glua_ffi", "eval_chunk")
fn do_eval_chunk(
lua: Lua,
chunk: Chunk,
) -> Result(#(Lua, List(Value)), Error(e))
/// Evaluates a Lua source file.
///
/// ## Examples
///
/// ```gleam
/// glua.eval_file(
/// path: "path/to/hello.lua",
/// )
/// |> glua.returning_multi(using: decode.string)
/// |> glua.run(glua.new(), _)
/// // -> Ok(["hello, world!"])
/// ```
///
/// ```gleam
/// glua.run(glua.new(), glua.eval_file(
/// path: "path/to/non/existent/file",
/// ))
/// // -> Error(glua.FileNotFound(["path/to/non/existent/file"]))
/// ```
pub fn eval_file(path path: String) -> Action(List(Value), e) {
Action(do_eval_file(_, path))
}
@external(erlang, "glua_ffi", "eval_file")
fn do_eval_file(lua: Lua, path: String) -> Result(#(Lua, List(Value)), Error(e))
/// Calls a Lua function by reference.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), {
/// use fun <- glua.then(
/// glua.eval(code: "return math.sqrt") |> glua.try(list.first)
/// )
///
/// glua.call_function(
/// ref: fun,
/// args: [glua.int(81)],
/// )
/// |> glua.returning_multi(using: decode.float)
/// })
/// // -> Ok([9.0])
/// ```
///
/// ```gleam
/// let code = "function fib(n)
/// if n <= 1 then
/// return n
/// else
/// return fib(n - 1) + fib(n - 2)
/// end
/// end
///
/// return fib
/// "
///
/// glua.run(glua.new(), {
/// use fun <- glua.then(glua.eval(code:) |> glua.try(list.first))
///
/// glua.call_function(
/// ref: fun,
/// args: [glua.int(10)],
/// )
/// |> glua.returning_multi(using: decode.int)
/// })
/// // -> Ok([55])
/// ```
pub fn call_function(
ref fun: Value,
args args: List(Value),
) -> Action(List(Value), e) {
Action(do_call_function(_, fun, args))
}
@external(erlang, "glua_ffi", "call_function")
fn do_call_function(
lua: Lua,
fun: Value,
args: List(Value),
) -> Result(#(Lua, List(Value)), Error(e))
/// Gets a reference to the function at `keys`, then inmediatly calls it with the provided `args`.
///
/// This is a shorthand for `glua.get` followed by `glua.call_function`.
///
/// ## Examples
///
/// ```gleam
/// glua.call_function_by_name(
/// keys: ["string", "upper"],
/// args: [glua.string("hello from Gleam!")]
/// ))
/// |> glua.returning_multi(using: decode.string)
/// |> glua.run(glua.new(), _)
/// // -> Ok("HELLO FROM GLEAM!")
/// ```
pub fn call_function_by_name(
keys keys: List(String),
args args: List(Value),
) -> Action(List(Value), e) {
use fun <- then(get(keys))
call_function(fun, args)
}
/// Gets the value at `keys` under the provided table.
///
/// This might trigger the `__index` metamethod.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), {
/// let fun = fn(_) {
/// glua.success([glua.string("fixed value")])
/// }
///
/// use tbl <- glua.then(glua.table([]))
/// use mt <- glua.then(glua.table([#(glua.string("__index"), glua.function(fun))]))
/// use _ <- glua.then(glua.call_function(lib.set_metatable(), [tbl, mt]))
///
/// glua.index(tbl, glua.string("a_key")) |> glua.returning(decode.string)
/// })
/// // -> Ok("fixed value")
/// ```
pub fn index(table ref: Value, key key: Value) -> Action(Value, e) {
Action(do_index(_, ref, key))
}
@external(erlang, "glua_ffi", "get_table_key")
fn do_index(
state: Lua,
ref: Value,
key: Value,
) -> Result(#(Lua, Value), Error(e))
/// Sets `value` under `key` of the provided table.
///
/// This might trigger the `__newindex` metamethod.
///
/// ## Examples
///
/// ```gleam
/// glua.run(glua.new(), {
/// let fun = fn(_) {
/// glua.error("this is a read-only table")
/// }
///
/// use tbl <- glua.then(glua.table([]))
/// use mt <- glua.then(glua.table([#(glua.string("__newindex"), glua.function(fun))]))
/// use _ <- glua.then(glua.call_function(lib.set_metatable(), [tbl, mt]))
///
/// glua.new_index(tbl, glua.string("my_new_key"), glua.string("my_new_value"))
/// })
/// // -> Error(glua.LuaRuntimeException(
/// exception: glua.ErrorCall("this is a read-only table", option.None),
/// state: _
/// ))
/// ```
pub fn new_index(
table ref: Value,
key key: Value,
value val: Value,
) -> Action(Nil, e) {
Action(do_new_index(_, ref, key, val))
}
@external(erlang, "glua_ffi", "set_table_key")
fn do_new_index(
state: Lua,
ref: Value,
key: Value,
val: Value,
) -> Result(#(Lua, Nil), Error(e))