diff --git a/README.md b/README.md index 3ef2b2d..db2effb 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Glua -A library for embedding Lua in Gleam applications! +A library for embedding Lua in Gleam applications powered by [Luerl](https://github.com/rvirding/luerl)! [![Package Version](https://img.shields.io/hexpm/v/glua)](https://hex.pm/packages/glua) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/glua/) ```sh -gleam add glua@1 +gleam add glua@2 ``` ## Usage @@ -14,7 +14,8 @@ gleam add glua@1 ### Executing Lua code ```gleam -let code = " + let code = + " function greet() return 'Hello from Lua!' end @@ -22,47 +23,54 @@ end return greet() " -let assert Ok(#(_state, [result])) = glua.eval( - state: glua.new(), - code:, - using: decode.string -) + // Make a fresh instance lua instance + let lua = glua.new() + let assert Ok(#(_state, [result])) = glua.eval(state: lua, code:) -assert result == "Hello from Lua!" + assert result == glua.string("Hello from Lua!") ``` -### Parsing a chunk, then executing it +### Decoding output +The `deser` api is similar to the `decode` +```gleam +pub type Project { + Project(name: String, language: String) +} +let code = "return { name = 'glua', written_in = 'gleam'}" +let assert Ok(#(lua, [table])) = glua.eval(glua.new(), code) + +let deserializer = { + use name <- deser.field(glua.string("name"), deser.string) + use language <- deser.field(glua.string("written_in"), deser.string) + deser.success(lua, Project(name:, language:)) +} + +let assert Ok(#(_lua, project)) = deser.run(lua, table, deserializer) +assert project == Project(name: "glua", language: "gleam") +``` +### Parsing a chunk, then executing it ```gleam let code = "return 'this is a chunk of Lua code'" let assert Ok(#(state, chunk)) = glua.load(state: glua.new(), code:) -let assert Ok(#(_state, [result])) = - glua.eval_chunk(state:, chunk:, using: decode.string) +let assert Ok(#(_state, [result])) = glua.eval_chunk(state:, chunk:) -assert result == "this is a chunk of Lua code" +assert result == glua.string("this is a chunk of Lua code") ``` ### Executing Lua files - ```gleam -let assert Ok(#(_state, [n, m])) = glua.eval_file( - state: glua.new(), - path: "./my_lua_files/two_numbers.lua" - using: decode.int -) +let assert Ok(#(_state, [n, m])) = + glua.eval_file(state: glua.new(), path: "./test/lua/two_numbers.lua") -assert n == 1 && m == 2 +assert n == glua.int(1) && m == glua.int(2) ``` ### Sandboxing - ```gleam let assert Ok(lua) = glua.new() |> glua.sandbox(["os", "execute"]) -let assert Error(glua.LuaRuntimeException(exception, _)) = glua.eval( - state: lua, - code: "os.execute('rm -f important_file'); return 0", - using: decode.int -) +let assert Error(glua.LuaRuntimeException(exception, _)) = + glua.eval(state: lua, code: "os.execute('rm -f important_file')") // 'important_file' was not deleted assert exception == glua.ErrorCall(["os.execute is sandboxed"]) @@ -71,139 +79,82 @@ assert exception == glua.ErrorCall(["os.execute is sandboxed"]) ### Getting values from Lua ```gleam -let assert Ok(version) = glua.get( - state: glua.new(), - keys: ["_VERSION"], - using: decode.string -) +let assert Ok(version) = glua.get(state: glua.new(), keys: ["_VERSION"]) -assert version == "Lua 5.3" +assert version == glua.string("Lua 5.3") ``` ### Setting values in Lua ```gleam // we need to encode any value we want to pass to Lua -let #(lua, encoded) = glua.string(glua.new(), "my_value") - +let lua = glua.new() // `keys` is the full path to where the value will be set // and any intermediate table will be created if it is not present let keys = ["my_table", "my_value"] -let assert Ok(lua) = glua.set(state: lua, keys:, value: encoded) +let assert Ok(lua) = glua.set(state: lua, keys:, value: glua.string("my_value")) // now we can get the value -let assert Ok(value) = glua.get(state: lua, keys:, using: decode.string) +let assert Ok(value) = glua.get(state: lua, keys:) // or return it from a Lua script let assert Ok(#(_lua, [returned])) = - glua.eval( - state: lua, - code: "return my_table.my_value", - using: decode.string, - ) - -assert value == "my_value" -assert returned == "my_value" -``` + glua.eval(state: lua, code: "return my_table.my_value") -```gleam -// we can also encode a list of tuples as a table to set it in Lua -let my_table = [ - #("my_first_value", 1.2), - #("my_second_value", 2.1) -] - -// the function we use to encode the keys and the function we use to encode the values -let encoders = #(glua.string, glua.float) - -let #(lua, encoded) = glua.new() |> glua.table(encoders, my_table) -let assert Ok(lua) = glua.set(state: lua, keys: ["my_table"], value: encoded) - -// now we can get its values -let assert Ok(#(lua, [result])) = glua.eval( - state: lua, - code: "return my_table.my_second_value", - using: decode.float -) - -assert result == 2.1 - -// or we can get the whole table and decode it back to a list of tuples -assert glua.get( - state: lua, - keys: ["my_table"], - using: glua.table_decoder(decode.string, decode.float) -) == Ok([ - #("my_first_value", 1.2), - #("my_second_value", 2.1) -]) +assert value == glua.string("my_value") +assert returned == glua.string("my_value") ``` ### Calling Lua functions from Gleam ```gleam -// here we use `ref_get` instead of `get` because we need a reference to the function -// and not a decoded value let lua = glua.new() -let assert Ok(fun) = glua.ref_get( - state: lua, - keys: ["math", "max"] -) - -// we need to encode each argument we pass to a Lua function -// `glua.list` encodes a list of values using a single encoder function -let #(lua, args) = glua.list(lua, glua.int, [1, 20, 7, 18]) - -let assert Ok(#(lua, [result])) = glua.call_function( - state: lua, - ref: fun, - args:, - using: decode.int -) - -assert result == 20 - -// `glua.call_function_by_name` is a shorthand for `glua.ref_get` followed by `glua.call_function` -let assert Ok(#(_lua, [result])) = glua.call_function_by_name( - state: lua, - keys: ["math", "max"], - args:, - using: decode.int -) - -assert result == 20 +let assert Ok(val) = glua.get(state: lua, keys: ["math", "max"]) +let assert Ok(#(lua, fun)) = deser.run(lua, val, deser.function) +let args = [1, 20, 7, 18] |> list.map(glua.int) + +let assert Ok(#(lua, [result])) = + glua.call_function(state: lua, fun: fun, args:) + +let assert Ok(#(lua, result)) = deser.run(lua, result, deser.number) + +assert result == 20.0 + +// `glua.call_function_by_name` is a shorthand for `glua.get` followed by `glua.call_function` +let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) + +let assert Ok(#(_lua, result)) = deser.run(lua, result, deser.number) + +assert result == 20.0 ``` ### Exposing Gleam functions to Lua ```gleam -let #(lua, fun) = { - use lua, args <- glua.function(glua.new()) - - let assert [x, min, max] = args - let assert Ok([x, min, max]) = list.try_map( - [x, min, max], - decode.run(_, decode.float) - ) +let lua = glua.new() +let #(lua, fun) = + glua.function(lua, fn(lua, args) { + // Since Gleam is a statically typed language, each and every argument must be decoded + let assert [x, min, max] = args + let assert Ok(#(lua, x)) = deser.run(lua, x, deser.number) + let assert Ok(#(lua, min)) = deser.run(lua, min, deser.number) + let assert Ok(#(lua, max)) = deser.run(lua, max, deser.number) - let result = float.clamp(x, min, max) + let result = float.clamp(x, min, max) - glua.list(lua, glua.float, [result]) -} + #(lua, [glua.float(result)]) + }) let keys = ["my_functions", "clamp"] let assert Ok(lua) = glua.set(state: lua, keys:, value: fun) -let #(lua, args) = glua.list(lua, glua.float, [2.3, 1.2, 2.1]) -let assert Ok(#(_lua, [result])) = glua.call_function_by_name( - state: lua, - keys:, - args:, - using: decode.float -) +let args = [2.3, 1.2, 2.1] |> list.map(glua.float) +let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys:, args:) -assert result == 2.1 +assert result == glua.float(2.1) ``` Further documentation can be found at . diff --git a/src/deser.gleam b/src/deser.gleam new file mode 100644 index 0000000..8aaf88a --- /dev/null +++ b/src/deser.gleam @@ -0,0 +1,573 @@ +//// The deserialize API is similar to the gleam/dynamic/decode API but has some differences. +//// +//// The main difference is that it can change the state of a lua program due to metatables. +//// If you do not wish to keep the changed state, discard the new state. + +import gleam/bit_array +import gleam/bool +import gleam/dict.{type Dict} +import gleam/dynamic +import gleam/float +import gleam/int +import gleam/list +import gleam/option.{type Option} +import gleam/string +import glua.{type Lua, type Value} + +pub opaque type Deserializer(t) { + Deserializer(function: fn(Lua, Value) -> Return(t)) +} + +pub type DeserializeError { + DeserializeError(expected: String, found: String, path: List(Value)) +} + +type Return(t) = + #(t, List(DeserializeError)) + +pub fn field( + field_path: Value, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + subfield([field_path], field_decoder, next) +} + +pub fn subfield( + field_path: List(Value), + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(function: fn(lua, data) { + let #(out, errors1) = + index_into( + lua, + field_path, + [], + field_decoder.function, + data, + fn(lua, data, position) { + let #(default, _) = field_decoder.function(lua, data) + #(default, [DeserializeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }, + ) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) + }) +} + +fn index_into( + lua: Lua, + path: List(Value), + position: List(Value), + inner: fn(Lua, Value) -> Return(b), + data: Value, + handle_miss: fn(Lua, Value, List(Value)) -> Return(b), +) -> Return(b) { + case path { + [] -> { + data + |> inner(lua, _) + |> push_path(list.reverse(position)) + } + + [key, ..path] -> { + case classify(data) { + "Table" -> + case get_table_key(lua, data, key) { + Ok(#(lua, data)) -> { + index_into(lua, path, [key, ..position], inner, data, handle_miss) + } + Error(Nil) -> { + handle_miss(lua, data, [key, ..position]) + } + } + class -> { + let #(default, _) = inner(lua, data) + #(default, [DeserializeError("Table", class, [])]) + |> push_path(list.reverse(position)) + } + } + } + } +} + +pub fn run( + lua: Lua, + data: Value, + deser: Deserializer(t), +) -> Result(t, List(DeserializeError)) { + let #(maybe_invalid_data, errors) = deser.function(lua, data) + case errors { + [] -> Ok(maybe_invalid_data) + [_, ..] -> Error(errors) + } +} + +/// Puts all values in a MultiValue, a special container that is only addressable with `deser.item` +pub fn run_multi(lua: Lua, data: List(Value), deser: Deserializer(t)) { + let list = new_mval(data) + run(lua, list, deser) +} + +@external(erlang, "glua_ffi", "new_mval") +fn new_mval(values: List(Value)) -> Value + +pub fn item( + field_path: Int, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(fn(lua, data) { + let class = classify(data) + let pos = glua.string(" int.to_string(field_path) <> ">") + + let fail = fn(error) { + let #(default, _) = field_decoder.function(lua, data) + let #(out, errors) = next(default).function(lua, data) + #(out, list.append([error], errors)) + } + use <- bool.lazy_guard(class != "MultiValue", fn() { + fail(DeserializeError("MultiValue", classify(data), [pos])) + }) + case get_entry(data, field_path) { + Ok(val) -> { + let #(out, errors1) = + val |> field_decoder.function(lua, _) |> push_path([pos]) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) + } + Error(Nil) -> fail(DeserializeError("Field", "Nothing", [pos])) + } + }) +} + +@external(erlang, "glua_ffi", "get_entry") +fn get_entry(mval: Value, idx: Int) -> Result(Value, Nil) + +pub fn error_to_string(error: DeserializeError) { + "Expected " + <> error.expected + <> " got " + <> error.found + <> " at [" + <> error.path |> list.map(string.inspect) |> string.join(", ") + <> "]" +} + +pub fn at(path: List(Value), inner: Deserializer(a)) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + index_into(lua, path, [], inner.function, data, fn(lua, data, position) { + let #(default, _) = inner.function(lua, data) + #(default, [DeserializeError("Field", "Nothing", [])]) + |> push_path(list.reverse(position)) + }) + }) +} + +@external(erlang, "glua_ffi", "get_table_key") +fn get_table_key( + lua: Lua, + table: Value, + key: Value, +) -> Result(#(Lua, Value), Nil) + +fn push_path(layer: Return(t), path: List(Value)) -> Return(t) { + let errors = + list.map(layer.1, fn(error) { + DeserializeError(..error, path: list.append(path, error.path)) + }) + #(layer.0, errors) +} + +pub fn success(data: t) -> Deserializer(t) { + Deserializer(function: fn(_lua, _) { #(data, []) }) +} + +pub fn deser_error( + expected expected: String, + found found: Value, +) -> List(DeserializeError) { + [DeserializeError(expected: expected, found: classify(found), path: [])] +} + +@external(erlang, "glua_ffi", "classify") +pub fn classify(a: anything) -> String + +pub fn optional_field( + key: Value, + default: t, + field_decoder: Deserializer(t), + next: fn(t) -> Deserializer(final), +) -> Deserializer(final) { + Deserializer(function: fn(lua, data) { + let class = classify(data) + use <- bool.lazy_guard(class != "Table", fn() { + next(default).function(lua, data) + }) + let #(out, errors1) = + case get_table_key(lua, data, key) { + Ok(#(lua, data)) -> { + field_decoder.function(lua, data) + } + Error(Nil) -> #(default, []) + } + |> push_path([key]) + let #(out, errors2) = next(out).function(lua, data) + #(out, list.append(errors1, errors2)) + }) +} + +pub fn optionally_at( + path: List(Value), + default: a, + inner: Deserializer(a), +) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + index_into(lua, path, [], inner.function, data, fn(_, _, _) { + #(default, []) + }) + }) +} + +fn run_dynamic_function( + lua: Lua, + data: Value, + expected: String, + zero: t, +) -> Return(t) { + let got = classify(data) + case got == expected { + True -> #(decode(data, lua), []) + False -> #(zero, [ + DeserializeError(expected, got, []), + ]) + } +} + +/// Warning: Can panic +@external(erlang, "luerl", "decode") +fn decode(a: Value, lua: Lua) -> a + +pub const string: Deserializer(String) = Deserializer(deser_string) + +fn deser_string(lua, data: Value) -> Return(String) { + run_dynamic_function(lua, data, "String", "") +} + +pub const byte_string: Deserializer(BitArray) = Deserializer(deser_byte_string) + +fn deser_byte_string(lua, data: Value) -> Return(BitArray) { + run_dynamic_function(lua, data, "ByteString", bit_array.from_string("")) +} + +pub const bool: Deserializer(Bool) = Deserializer(deser_bool) + +fn deser_bool(lua, data: Value) -> Return(Bool) { + run_dynamic_function(lua, data, "Bool", True) +} + +pub const number: Deserializer(Float) = Deserializer(deser_num) + +fn deser_num(lua, data: Value) -> Return(Float) { + let got = classify(data) + case got { + "Float" -> #(decode(data, lua), []) + "Int" -> { + let int: Int = decode(data, lua) + #(int.to_float(int), []) + } + _ -> #(0.0, [ + DeserializeError("Number", got, []), + ]) + } +} + +pub const int: Deserializer(Int) = Deserializer(deser_int) + +fn deser_int(lua, data: Value) -> Return(Int) { + let got = classify(data) + let error = #(0, [ + DeserializeError("Int", got, []), + ]) + case got { + "Float" -> { + let float: Float = decode(data, lua) + let int = float.truncate(float) + case int.to_float(int) == float { + True -> #(int, []) + False -> error + } + } + "Int" -> { + #(decode(data, lua), []) + } + _ -> error + } +} + +pub const raw: Deserializer(Value) = Deserializer(decode_raw) + +fn decode_raw(_lua, data: Value) -> Return(Value) { + let class = classify(data) + case class { + "Unknown" + | // Disallow smuggling MultiValues out + "MultiValue" -> #(glua.nil(), [DeserializeError("Any", class, [])]) + _ -> #(data, []) + } +} + +pub const userdata: Deserializer(dynamic.Dynamic) = Deserializer( + deser_user_defined, +) + +@external(erlang, "glua_ffi", "unwrap_userdata") +fn unwrap_userdata(a: userdata) -> Result(dynamic.Dynamic, Nil) + +fn deser_user_defined(lua, data: Value) -> Return(dynamic.Dynamic) { + let got = classify(data) + let error = #(dynamic.nil(), [DeserializeError("UserData", got, [])]) + use <- bool.guard(got != "UserData", error) + use <- bool.guard( + !userdata_exists(lua, data), + #(dynamic.nil(), [ + DeserializeError("UserData", "NonexistentUserData", []), + ]), + ) + case unwrap_userdata(decode(data, lua)) { + Ok(dyn) -> #(dyn, []) + Error(Nil) -> error + } +} + +pub const function: Deserializer(glua.Function) = Deserializer(deser_function) + +fn deser_function(_lua: Lua, data: Value) -> Return(glua.Function) { + let got = classify(data) + case got == "Function" { + True -> #(coerce_funciton(data), []) + False -> #(coerce_funciton(Nil), [ + DeserializeError("Function", got, []), + ]) + } +} + +@external(erlang, "glua_ffi", "coerce") +fn coerce_funciton(func: anything) -> glua.Function + +/// Strictly decodes a list +/// 1. first element must start with 1 +/// 2. no gaps +/// 3. no non number keys +pub fn list(of inner: Deserializer(a)) -> Deserializer(List(a)) { + Deserializer(fn(lua, data) { + let class = classify(data) + let not_table_list = #([], [DeserializeError("TableList", class, [])]) + case class { + "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #([], [ + DeserializeError("TableList", "NonexistentTable", []), + ]), + ) + let res = + get_table_list_transform(lua, data, #([], []), fn(it, acc) { + case acc.1 { + [] -> { + case inner.function(lua, it) { + #(value, []) -> { + #(list.prepend(acc.0, value), acc.1) + } + #(_, errors) -> + push_path(#([], errors), [glua.string("items")]) + } + } + [_, ..] -> acc + } + }) + case res { + Ok(it) -> it + Error(Nil) -> not_table_list + } + } + _ -> not_table_list + } + }) +} + +@external(erlang, "glua_ffi", "get_table_list_transform") +fn get_table_list_transform( + lua: Lua, + table: Value, + accumulator: acc, + func: fn(Value, acc) -> acc, +) -> Result(acc, Nil) + +pub fn dict( + key: Deserializer(key), + value: Deserializer(value), +) -> Deserializer(Dict(key, value)) { + Deserializer(fn(lua, data) { + let class = classify(data) + case class { + "Table" -> { + use <- bool.guard( + !table_exists(lua, data), + #(dict.new(), [DeserializeError("Table", "NonexistentTable", [])]), + ) + get_table_transform(lua, data, #(dict.new(), []), fn(k, v, a) { + // If there are any errors from previous key-value pairs then we + // don't need to run the decoders, instead return the existing acc. + case a.1 { + [] -> fold_dict(lua, a, k, v, key.function, value.function) + [_, ..] -> a + } + }) + } + _ -> #(dict.new(), [DeserializeError("Table", class, [])]) + } + }) +} + +@external(erlang, "glua_ffi", "table_exists") +fn table_exists(state: Lua, table: Value) -> Bool + +@external(erlang, "glua_ffi", "userdata_exists") +fn userdata_exists(state: Lua, userdata: Value) -> Bool + +/// Preconditions: `table` is a lua table +@external(erlang, "glua_ffi", "get_table_transform") +fn get_table_transform( + lua: Lua, + table: Value, + accumulator: acc, + func: fn(Value, Value, acc) -> acc, +) -> acc + +fn fold_dict( + lua: Lua, + acc: #(Dict(k, v), List(DeserializeError)), + key: Value, + value: Value, + key_decoder: fn(Lua, Value) -> Return(k), + value_decoder: fn(Lua, Value) -> Return(v), +) -> Return(Dict(k, v)) { + // First we decode the key. + case key_decoder(lua, key) { + #(key, []) -> + // Then we decode the value. + case value_decoder(lua, value) { + #(value, []) -> { + // It worked! Insert the new key-value pair so we can move onto the next. + let dict = dict.insert(acc.0, key, value) + #(dict, acc.1) + } + #(_, errors) -> + push_path(#(dict.new(), errors), [glua.string("values")]) + } + #(_, errors) -> push_path(#(dict.new(), errors), [glua.string("keys")]) + } +} + +pub fn optional(inner: Deserializer(a)) -> Deserializer(Option(a)) { + Deserializer(function: fn(lua, data) { + case classify(data) { + "Nil" -> #(option.None, []) + _ -> { + let #(data, errors) = inner.function(lua, data) + #(option.Some(data), errors) + } + } + }) +} + +pub fn map(decoder: Deserializer(a), transformer: fn(a) -> b) -> Deserializer(b) { + Deserializer(function: fn(lua, d) { + let #(data, errors) = decoder.function(lua, d) + #(transformer(data), errors) + }) +} + +pub fn map_errors( + decoder: Deserializer(a), + transformer: fn(List(DeserializeError)) -> List(DeserializeError), +) -> Deserializer(a) { + Deserializer(function: fn(lua, d) { + let #(data, errors) = decoder.function(lua, d) + #(data, transformer(errors)) + }) +} + +pub fn collapse_errors( + decoder: Deserializer(a), + name: String, +) -> Deserializer(a) { + Deserializer(function: fn(lua, dynamic_data) { + let #(data, errors) as layer = decoder.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, deser_error(name, dynamic_data)) + } + }) +} + +pub fn then( + decoder: Deserializer(a), + next: fn(Lua, a) -> Deserializer(b), +) -> Deserializer(b) { + Deserializer(function: fn(lua, dynamic_data) { + let #(data, errors) = decoder.function(lua, dynamic_data) + let decoder = next(lua, data) + let #(data, _) as layer = decoder.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> #(data, errors) + } + }) +} + +pub fn one_of( + first: Deserializer(a), + or alternatives: List(Deserializer(a)), +) -> Deserializer(a) { + Deserializer(function: fn(lua, dynamic_data) { + let #(_, errors) as layer = first.function(lua, dynamic_data) + case errors { + [] -> layer + [_, ..] -> run_decoders(dynamic_data, lua, layer, alternatives) + } + }) +} + +fn run_decoders( + data: Value, + lua: Lua, + failure: Return(a), + decoders: List(Deserializer(a)), +) -> Return(a) { + case decoders { + [] -> failure + + [decoder, ..decoders] -> { + let #(_, errors) as layer = decoder.function(lua, data) + case errors { + [] -> layer + [_, ..] -> run_decoders(data, lua, failure, decoders) + } + } + } +} + +pub fn failure(zero: a, expected: String) -> Deserializer(a) { + Deserializer(function: fn(_lua, d) { #(zero, deser_error(expected, d)) }) +} + +pub fn recursive(inner: fn() -> Deserializer(a)) -> Deserializer(a) { + Deserializer(function: fn(lua, data) { + let decoder = inner() + decoder.function(lua, data) + }) +} diff --git a/src/glua.gleam b/src/glua.gleam index cc5ae51..73e31f3 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -3,8 +3,8 @@ //// Gleam wrapper around [Luerl](https://github.com/rvirding/luerl). import gleam/dynamic -import gleam/dynamic/decode import gleam/list +import gleam/pair import gleam/result import gleam/string @@ -19,8 +19,6 @@ pub type LuaError { LuaRuntimeException(exception: LuaRuntimeExceptionKind, state: Lua) /// A certain key was not found in the Lua environment. KeyNotFound - /// The value returned by the Lua environment could not be decoded using the provided decoder. - UnexpectedResultType(List(decode.DecodeError)) /// An error that could not be identified. UnknownError } @@ -37,22 +35,18 @@ pub type LuaRuntimeExceptionKind { BadArith(operator: String, args: List(String)) /// 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 + /// An exception that could not be identified. + UnknownException(dynamic.Dynamic) } -/// The exception that happens when a functi /// 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. +/// Represents an encoded value inside the Lua environment. pub type Value -/// Represents a reference to a value inside the Lua environment. -/// -/// Each one of the functions that returns values from the Lua environment has a `ref_` counterpart -/// that will return references to the values instead of decoding them. -pub type ValueRef +/// A `Value` that is a function +pub type Function @external(erlang, "glua_ffi", "coerce_nil") pub fn nil() -> Value @@ -69,88 +63,34 @@ pub fn int(v: Int) -> Value @external(erlang, "glua_ffi", "coerce") pub fn float(v: Float) -> Value -@external(erlang, "glua_ffi", "coerce") -pub fn table(values: List(#(Value, Value))) -> Value - -pub fn table_decoder( - keys_decoder: decode.Decoder(a), - values_decoder: decode.Decoder(b), -) -> decode.Decoder(List(#(a, b))) { - let inner = { - use key <- decode.field(0, keys_decoder) - use val <- decode.field(1, values_decoder) - decode.success(#(key, val)) - } - - decode.list(of: inner) -} +@external(erlang, "glua_ffi", "encode_table") +pub fn table(lua: Lua, values: List(#(Value, Value))) -> #(Lua, Value) -pub fn function( - f: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value { - // we need a little wrapper for functions to satisfy luerl's order of arguments and return value type - wrap_function(f) +pub fn table_list(lua: Lua, values: List(Value)) -> #(Lua, Value) { + list.map_fold(values, 1, fn(acc, val) { #(acc + 1, #(int(acc), val)) }) + |> pair.second() + |> table(lua, _) } -pub fn list(encoder: fn(a) -> Value, values: List(a)) -> List(Value) { - list.map(values, encoder) -} - -/// 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:)) -/// } -/// -/// let state = glua.new() -/// let assert Ok(state) = glua.set( -/// state:, -/// keys: ["a_user"], -/// value: glua.userdata(User(name: "Jhon Doe", is_admin: False)) -/// ) -/// -/// let assert Ok(#(_, [result])) = glua.eval(state:, code: "return a_user", using: user_decoder) -/// assert result == User("Jhon Doe", False) -/// ``` -/// -/// ```gleam -/// pub type Person { -/// Person(name: String, email: String) -/// } -/// -/// let state = glua.new() -/// let assert Ok(lua) = glua.set( -/// state:, -/// keys: ["lucy"], -/// value: glua.userdata(Person(name: "Lucy", email: "lucy@example.com")) -/// ) -/// -/// let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_), _)) = -/// glua.eval(state:, code: "return lucy.email", using: decode.string) -/// ``` -@external(erlang, "glua_ffi", "coerce_userdata") -pub fn userdata(v: anything) -> Value +@external(erlang, "glua_ffi", "encode_userdata") +pub fn userdata(lua: Lua, val: anything) -> #(Lua, Value) @external(erlang, "glua_ffi", "wrap_fun") -fn wrap_function( - fun: fn(Lua, List(dynamic.Dynamic)) -> #(Lua, List(Value)), -) -> Value +pub fn function( + fun: fn(Lua, List(Value)) -> Result(#(Lua, List(Value)), #(Lua, List(String))), +) -> Function + +/// Downgrade a `Function` to a `Value` +@external(erlang, "glua_ffi", "coerce") +pub fn func_to_val(func: Function) -> Value /// Creates a new Lua VM instance @external(erlang, "luerl", "init") pub fn new() -> Lua +@external(erlang, "glua_ffi", "classify_type") +pub fn typeof(val: Value) -> String + /// List of Lua modules and functions that will be sandboxed by default pub const default_sandbox = [ ["io"], @@ -202,54 +142,12 @@ fn list_substraction(a: List(a), b: List(a)) -> List(a) /// ``` pub fn sandbox(state lua: Lua, keys keys: List(String)) -> Result(Lua, LuaError) { let msg = string.join(keys, with: ".") <> " is sandboxed" - set(lua, ["_G", ..keys], sandbox_fun(msg)) + let #(fun, lua) = sandbox_fun(lua, msg) + set(lua, ["_G", ..keys], fun) } @external(erlang, "glua_ffi", "sandbox_fun") -fn sandbox_fun(msg: String) -> Value - -/// Gets a value in the Lua environment. -/// -/// ## Examples -/// -/// ```gleam -/// glua.get(state: glua.new(), keys: ["_VERSION"], using: decode.string) -/// // -> Ok("Lua 5.3") -/// ``` -/// -/// ```gleam -/// let #(lua, encoded) = glua.new() |> glua.bool(True) -/// let assert Ok(lua) = glua.set( -/// state: lua, -/// keys: ["my_table", "my_value"], -/// value: encoded -/// ) -/// -/// glua.get( -/// state: lua, -/// keys: ["my_table", "my_value"], -/// using: decode.bool -/// ) -/// // -> Ok(True) -/// ``` -/// -/// ```gleam -/// glua.get(state: glua.new(), keys: ["non_existent"], using: decode.string) -/// // -> Error(glua.KeyNotFound) -/// ``` -pub fn get( - state lua: Lua, - keys keys: List(String), - using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { - use value <- result.try(do_get(lua, keys)) - - use decoded <- result.try( - decode.run(value, decoder) |> result.map_error(UnexpectedResultType), - ) - - Ok(decoded) -} +fn sandbox_fun(state: Lua, msg: String) -> #(Value, Lua) /// Gets a private value that is not exposed to the Lua runtime. /// @@ -264,22 +162,14 @@ pub fn get( pub fn get_private( state lua: Lua, key key: String, - using decoder: decode.Decoder(a), -) -> Result(a, LuaError) { +) -> Result(dynamic.Dynamic, LuaError) { use value <- result.try(do_get_private(lua, key)) - use decoded <- result.try( - decode.run(value, decoder) |> result.map_error(UnexpectedResultType), - ) - - Ok(decoded) + Ok(value) } /// Same as `glua.get`, but returns a reference to the value instead of decoding it -pub fn ref_get( - state lua: Lua, - keys keys: List(String), -) -> Result(ValueRef, LuaError) { - do_ref_get(lua, keys) +pub fn get(state lua: Lua, keys keys: List(String)) -> Result(Value, LuaError) { + do_get(lua, keys) } /// Sets a value in the Lua environment. @@ -329,11 +219,11 @@ pub fn set( use acc, key <- list.try_fold(keys, #([], lua)) let #(keys, lua) = acc let keys = list.append(keys, [key]) - case do_ref_get(lua, keys) { + case do_get(lua, keys) { Ok(_) -> Ok(#(keys, lua)) Error(KeyNotFound) -> { - let #(tbl, lua) = alloc_table([], lua) + let #(tbl, lua) = do_alloc_table([], lua) do_set(lua, keys, tbl) |> result.map(fn(lua) { #(keys, lua) }) } @@ -400,17 +290,15 @@ pub fn set_lua_paths( set(lua, ["package", "path"], paths) } -@external(erlang, "luerl_emul", "alloc_table") -fn alloc_table(content: List(a), lua: Lua) -> #(a, Lua) - -@external(erlang, "glua_ffi", "get_table_keys_dec") -fn do_get(lua: Lua, keys: List(String)) -> Result(dynamic.Dynamic, LuaError) +// TODO: Fix +@external(erlang, "luerl_heap", "alloc_table") +fn do_alloc_table(content: List(a), lua: Lua) -> #(Value, Lua) @external(erlang, "glua_ffi", "get_private") fn do_get_private(lua: Lua, key: String) -> Result(dynamic.Dynamic, LuaError) @external(erlang, "glua_ffi", "get_table_keys") -fn do_ref_get(lua: Lua, keys: List(String)) -> Result(ValueRef, LuaError) +fn do_get(lua: Lua, keys: List(String)) -> Result(Value, LuaError) @external(erlang, "glua_ffi", "set_table_keys") fn do_set(lua: Lua, keys: List(String), val: a) -> Result(Lua, LuaError) @@ -463,292 +351,66 @@ pub fn load_file( @external(erlang, "glua_ffi", "load_file") fn do_load_file(lua: Lua, path: String) -> Result(#(Lua, Chunk), LuaError) -/// Evaluates a string of Lua code. -/// -/// ## Examples -/// -/// ```gleam -/// let assert Ok(#(_, results)) = glua.eval( -/// state: glua.new(), -/// code: "return 1 + 2", -/// using: decode.int -/// ) -/// assert results == [3] -/// ``` -/// -/// ```gleam -/// let my_decoder = decode.one_of(decode.string, or: [ -/// decode.int |> decode.map(int.to_string) -/// ]) -/// -/// let assert Ok(#(_, results)) = glua.eval( -/// state: glua.new(), -/// code: "return 'hello, world!', 10", -/// using: my_decoder -/// ) -/// assert results == ["hello, world!", "10"] -/// ``` -/// -/// ```gleam -/// glua.eval(state: glua.new(), code: "return 1 * ", using: decode.int) -/// // -> Error(glua.LuaCompilerException( -/// messages: ["syntax error before: ", "1"] -/// )) -/// ``` -/// -/// ```gleam -/// glua.eval(state: glua.new(), code: "return 'Hello, world!'", using: decode.int) -/// // -> Error(glua.UnexpectedResultType( -/// [decode.DecodeError("Int", "String", [])] -/// )) -/// ``` -/// -/// > **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` or `glua.ref_eval_chunk`. -pub fn eval( - state lua: Lua, - code code: String, - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_eval(lua, code)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} - -@external(erlang, "glua_ffi", "eval_dec") -fn do_eval( - lua: Lua, - code: String, -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) - /// Same as `glua.eval`, but returns references to the values instead of decode them -pub fn ref_eval( +pub fn eval( state lua: Lua, code code: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_eval(lua, code) +) -> Result(#(Lua, List(Value)), LuaError) { + do_eval(lua, code) } @external(erlang, "glua_ffi", "eval") -fn do_ref_eval( - lua: Lua, - code: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) - -/// Evaluates a compiled chunk of Lua code. -/// -/// ## Examples -/// ```gleam -/// let assert Ok(#(lua, chunk)) = glua.load( -/// state: glua.new(), -/// code: "return 'hello, world!'" -/// ) -/// -/// let assert Ok(#(_, results)) = glua.eval_chunk( -/// state: lua, -/// chunk:, -/// using: decode.string -/// ) -/// -/// assert results == ["hello, world!"] -/// ``` -pub fn eval_chunk( - state lua: Lua, - chunk chunk: Chunk, - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_eval_chunk(lua, chunk)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} - -@external(erlang, "glua_ffi", "eval_chunk_dec") -fn do_eval_chunk( - lua: Lua, - chunk: Chunk, -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) +fn do_eval(lua: Lua, code: String) -> Result(#(Lua, List(Value)), LuaError) /// Same as `glua.eval_chunk`, but returns references to the values instead of decode them -pub fn ref_eval_chunk( +pub fn eval_chunk( state lua: Lua, chunk chunk: Chunk, -) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_eval_chunk(lua, chunk) +) -> Result(#(Lua, List(Value)), LuaError) { + do_eval_chunk(lua, chunk) } @external(erlang, "glua_ffi", "eval_chunk") -fn do_ref_eval_chunk( +fn do_eval_chunk( lua: Lua, chunk: Chunk, -) -> Result(#(Lua, List(ValueRef)), LuaError) - -/// Evaluates a Lua source file. -/// -/// ## Examples -/// ```gleam -/// let assert Ok(#(_, results)) = glua.eval_file( -/// state: glua.new(), -/// path: "path/to/hello.lua", -/// using: decode.string -/// ) -/// -/// assert results == ["hello, world!"] -/// ``` -pub fn eval_file( - state lua: Lua, - path path: String, - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_eval_file(lua, path)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} - -@external(erlang, "glua_ffi", "eval_file_dec") -fn do_eval_file( - lua: Lua, - path: String, -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) +) -> Result(#(Lua, List(Value)), LuaError) /// Same as `glua.eval_file`, but returns references to the values instead of decode them. -pub fn ref_eval_file( +pub fn eval_file( state lua: Lua, path path: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_eval_file(lua, path) +) -> Result(#(Lua, List(Value)), LuaError) { + do_eval_file(lua, path) } @external(erlang, "glua_ffi", "eval_file") -fn do_ref_eval_file( - lua: Lua, - path: String, -) -> Result(#(Lua, List(ValueRef)), LuaError) - -/// Calls a Lua function by reference. -/// -/// ## Examples -/// ```gleam -/// let assert Ok(#(lua, fun)) = glua.ref_eval(state: glua.new(), code: "return math.sqrt") -/// -/// let #(lua, encoded) = glua.int(lua, 81) -/// let assert Ok(#(_, [result])) = glua.call_function( -/// state: lua, -/// ref: fun, -/// args: [encoded], -/// using: decode.int -/// ) -/// -/// assert result == 9 -/// ``` -/// -/// ```gleam -/// let code = "function fib(n) -/// if n <= 1 then -/// return n -/// else -/// return fib(n - 1) + fib(n - 2) -/// end -/// end -/// -/// return fib -/// " -/// let assert Ok(#(lua, fun)) = glua.ref_eval(state: glua.new(), code:) -/// -/// let #(lua, encoded) = glua.int(lua, 10) -/// let assert Ok(#(_, [result])) = glua.call_function( -/// state: lua, -/// ref: fun, -/// args: [encoded], -/// using: decode.int -/// ) -/// -/// assert result == 55 -/// ``` -pub fn call_function( - state lua: Lua, - ref fun: ValueRef, - args args: List(Value), - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use #(lua, ret) <- result.try(do_call_function(lua, fun, args)) - use decoded <- result.try( - list.try_map(ret, decode.run(_, decoder)) - |> result.map_error(UnexpectedResultType), - ) - - Ok(#(lua, decoded)) -} +fn do_eval_file(lua: Lua, path: String) -> Result(#(Lua, List(Value)), LuaError) -@external(erlang, "glua_ffi", "call_function_dec") +@external(erlang, "glua_ffi", "call_function") fn do_call_function( lua: Lua, - fun: ValueRef, + fun: Function, args: List(Value), -) -> Result(#(Lua, List(dynamic.Dynamic)), LuaError) +) -> Result(#(Lua, List(Value)), LuaError) -/// Same as `glua.call_function`, but returns references to the values instead of decode them. -pub fn ref_call_function( +pub fn call_function( state lua: Lua, - ref fun: ValueRef, + fun fun: Function, args args: List(Value), -) -> Result(#(Lua, List(ValueRef)), LuaError) { - do_ref_call_function(lua, fun, args) +) -> Result(#(Lua, List(Value)), LuaError) { + do_call_function(lua, fun, args) } -@external(erlang, "glua_ffi", "call_function") -fn do_ref_call_function( - lua: Lua, - fun: ValueRef, - args: List(Value), -) -> Result(#(Lua, List(ValueRef)), LuaError) - -/// Gets a reference to the function at `keys`, then inmediatly calls it with the provided `args`. -/// -/// This is a shorthand for `glua.ref_get` followed by `glua.call_function`. -/// -/// ## Examples -/// -/// ```gleam -/// let #(lua, encoded) = glua.new() |> glua.string("hello from gleam!") -/// let assert Ok(#(_, [s])) = glua.call_function_by_name( -/// state: lua, -/// keys: ["string", "upper"], -/// args: [encoded], -/// using: decode.string -/// ) -/// -/// assert s == "HELLO FROM GLEAM!" -/// ``` +/// Same as `glua.call_function_by_name`, but it chains `glua.ref_get` with `glua.ref_call_function` instead of `glua.call_function` pub fn call_function_by_name( state lua: Lua, keys keys: List(String), args args: List(Value), - using decoder: decode.Decoder(a), -) -> Result(#(Lua, List(a)), LuaError) { - use fun <- result.try(ref_get(lua, keys)) - call_function(lua, fun, args, decoder) +) -> Result(#(Lua, List(Value)), LuaError) { + use fun <- result.try(get(lua, keys)) + call_function(lua, coerce_function(fun), args) } -/// Same as `glua.call_function_by_name`, but it chains `glua.ref_get` with `glua.ref_call_function` instead of `glua.call_function` -pub fn ref_call_function_by_name( - state lua: Lua, - keys keys: List(String), - args args: List(Value), -) -> Result(#(Lua, List(ValueRef)), LuaError) { - use fun <- result.try(ref_get(lua, keys)) - ref_call_function(lua, fun, args) -} +@external(erlang, "glua_ffi", "coerce") +fn coerce_function(func: Value) -> Function diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index 1edf9e7..1bd2d6a 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -1,28 +1,43 @@ -module(glua_ffi). -import(luerl_lib, [lua_error/2]). +-include_lib("luerl/include/luerl.hrl"). --export([coerce/1, coerce_nil/0, coerce_userdata/1, wrap_fun/1, sandbox_fun/1, get_table_keys/2, get_table_keys_dec/2, - get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_dec/2, eval_file/2, - eval_file_dec/2, eval_chunk/2, eval_chunk_dec/2, call_function/3, call_function_dec/3]). +-export([coerce/1, + coerce_nil/0, + coerce_userdata/1, + wrap_fun/1, + sandbox_fun/2, + get_table_keys/2, + get_table_key/3, + get_private/2, + set_table_keys/3, + load/2, + load_file/2, + eval/2, + eval_file/2, + encode_table/2, + encode_userdata/2, + eval_chunk/2, + call_function/3, + classify/1, + unwrap_userdata/1, + get_table_transform/4, + new_mval/1, + get_entry/2, + get_table_list_transform/4, + userdata_exists/2, + table_exists/2]). -%% turn `{userdata, Data}` into `Data` to make it more easy to decode it in Gleam -maybe_process_userdata(Lst) when is_list(Lst) -> - lists:map(fun maybe_process_userdata/1, Lst); -maybe_process_userdata({userdata, Data}) -> - Data; -maybe_process_userdata(Other) -> - Other. %% helper to convert luerl return values to a format %% that is more suitable for use in Gleam code to_gleam(Value) -> case Value of {ok, Result, LuaState} -> - Values = maybe_process_userdata(Result), - {ok, {LuaState, Values}}; + {ok, {LuaState, Result}}; {ok, _} = Result -> - maybe_process_userdata(Result); + Result; {lua_error, _, _} = Error -> {error, map_error(Error)}; {error, _, _} = Error -> @@ -31,32 +46,88 @@ to_gleam(Value) -> {error, unknown_error} end. -%% helper to determine if a value is encoded or not -%% borrowed from https://github.com/tv-labs/lua/blob/main/lib/lua/util.ex#L19-L35 -is_encoded(nil) -> - true; -is_encoded(true) -> - true; -is_encoded(false) -> - true; -is_encoded(Binary) when is_binary(Binary) -> - true; -is_encoded(N) when is_number(N) -> - true; -is_encoded({tref,_}) -> - true; -is_encoded({usrdef,_}) -> - true; -is_encoded({eref,_}) -> - true; -is_encoded({funref,_,_}) -> - true; -is_encoded({erl_func,_}) -> - true; -is_encoded({erl_mfa,_,_,_}) -> - true; -is_encoded(_) -> - false. + +classify(nil) -> + <<"Nil">>; +classify(Bool) when is_boolean(Bool) -> + <<"Bool">>; +classify(Binary) when is_binary(Binary) -> + case gleam@bit_array:is_utf8(Binary) of + true -> <<"String">>; + false -> <<"ByteString">> + end; +classify(N) when is_integer(N) -> + <<"Int">>; +classify(N) when is_float(N) -> + <<"Float">>; +classify({tref, _}) -> + <<"Table">>; +classify({usdref, _}) -> + <<"UserData">>; +classify({eref, _}) -> + <<"Unknown">>; +classify({funref, _, _}) -> + <<"Function">>; +classify({erl_func, _}) -> + <<"Function">>; +classify({erl_mfa, _, _, _}) -> + <<"Function">>; +classify(MVal) when is_tuple(MVal), + tuple_size(MVal) >= 1, + element(1, MVal) =:= multi_value -> + <<"MultiValue">>; +classify(_) -> + <<"Unknown">>. + +new_mval(MVal) when is_list(MVal) -> + NewMVal = [multi_value | MVal], + list_to_tuple(NewMVal). + +get_entry(MVal, Idx) when is_tuple(MVal), + element(1, MVal) =:= multi_value -> + Pos = Idx + 1, + case tuple_size(MVal) >= Pos andalso Pos >= 1 of + true -> {ok, element(Pos, MVal)}; + false -> {error, nil} + end. + +get_table_transform(St, #tref{} = T, Acc, Func) + when is_function(Func, 3) -> + #table{a = Arr, d = Dict} = luerl_heap:get_table(T, St), + Acc1 = ttdict:fold(Func, Acc, Dict), + array:sparse_foldl(Func, Acc1, Arr). + + +get_table_list_transform(St, #tref{} = T, Acc, Func) + when is_function(Func, 2) -> + #table{a = Arr, d = Dict} = luerl_heap:get_table(T, St), + case Dict of + empty -> do_list_transform(Arr, Acc, Func); + _ -> {error, nil} + end. + + +do_list_transform(Arr, Acc, Func) -> + N = array:size(Arr), + try + Wrapped = fun(Idx, Val, {Expected, UserAcc}) -> + case Idx of + Expected -> + {Expected - 1, Func(Val, UserAcc)}; + _ -> throw(hole) + end + end, + {End, Final} = + array:sparse_foldr(Wrapped, {N - 1, Acc}, Arr), + case {N, End} of + {0, _} -> {ok, Final}; + {_, 0} -> {ok, Final}; + _ -> {error, nil} + end + catch + throw:hole -> {error, nil} + end. + %% TODO: Improve compiler errors handling and try to detect more errors map_error({error, [{_, luerl_parse, Errors} | _], _}) -> @@ -76,49 +147,100 @@ map_error({lua_error, {badarith, Operator, Args}, State}) -> FormattedOperator = unicode:characters_to_binary(atom_to_list(Operator)), FormattedArgs = lists:map(fun(V) -> - unicode:characters_to_binary( - io_lib:format("~p", [V])) + unicode:characters_to_binary( + io_lib:format("~p", [V])) end, Args), {lua_runtime_exception, {bad_arith, FormattedOperator, FormattedArgs}, State}; map_error({lua_error, {assert_error, _} = Error, State}) -> {lua_runtime_exception, Error, State}; -map_error({lua_error, _, State}) -> - {lua_runtime_exception, unknown_exception, State}; +map_error({lua_error, Dynamic, State}) -> + {lua_runtime_exception, {unknown_exception, Dynamic}, State}; map_error(_) -> unknown_error. + coerce(X) -> X. + coerce_nil() -> nil. + coerce_userdata(X) -> {userdata, X}. + +unwrap_userdata({userdata, Data}) -> + {ok, Data}; +unwrap_userdata(_) -> + {error, nil}. + + +encode_table(State, Values) -> + {Data, St} = luerl_heap:alloc_table(Values, State), + {St, Data}. + + +encode_userdata(State, Values) -> + {Data, St} = luerl_heap:alloc_userdata(Values, State), + {St, Data}. + + wrap_fun(Fun) -> - fun(Args, State) -> - Decoded = luerl:decode_list(Args, State), - {NewState, Ret} = Fun(State, Decoded), - luerl:encode_list(Ret, NewState) + NewFun = fun(Args, StateIn) -> + {Status, Result} = Fun(StateIn, Args), + case Status of + ok -> + {StateOut, Return} = Result, + {Return, StateOut}; + error -> + {StateOut, Msgs} = Result, + {error, map_error(lua_error({error_call, Msgs}, StateOut))} + end + end, + #erl_func{code = NewFun}. + + +sandbox_fun(St, Msg) -> + Fun = fun(_, State) -> + {error, map_error(lua_error({error_call, [Msg]}, State))} + end, + luerl:encode(Fun, St). + + +table_exists(State, Table) -> + case luerl_heap:chk_table(Table, State) of + ok -> true; + error -> false end. -sandbox_fun(Msg) -> - fun(_, State) -> {error, map_error(lua_error({error_call, [Msg]}, State))} end. -get_table_keys(Lua, Keys) -> - case luerl:get_table_keys(Keys, Lua) of +userdata_exists(State, Table) -> + case luerl_heap:chk_userdata(Table, State) of + ok -> true; + error -> false + end. + + +get_table_key(_Lua, Args, _Key) when is_tuple(Args), + element(1, Args) =:= func_args -> + {error, nil}; + +get_table_key(Lua, Table, Key) -> + case luerl:get_table_key(Table, Key, Lua) of {ok, nil, _} -> - {error, key_not_found}; - {ok, Value, _} -> - {ok, Value}; - Other -> - to_gleam(Other) + {error, nil}; + {ok, Value, Lua} -> + {ok, {Lua, Value}}; + _Other -> + {error, nil} end. -get_table_keys_dec(Lua, Keys) -> - case luerl:get_table_keys_dec(Keys, Lua) of + +get_table_keys(Lua, Keys) -> + case luerl:get_table_keys(Keys, Lua) of {ok, nil, _} -> {error, key_not_found}; {ok, Value, _} -> @@ -127,56 +249,38 @@ get_table_keys_dec(Lua, Keys) -> to_gleam(Other) end. + set_table_keys(Lua, Keys, Value) -> - SetFun = case is_encoded(Value) of - true -> fun luerl:set_table_keys/3; - false -> fun luerl:set_table_keys_dec/3 - end, - to_gleam(SetFun(Keys, Value, Lua)). + to_gleam(luerl:set_table_keys(Keys, Value, Lua)). + load(Lua, Code) -> to_gleam(luerl:load( - unicode:characters_to_list(Code), Lua)). + unicode:characters_to_list(Code), Lua)). + load_file(Lua, Path) -> to_gleam(luerl:loadfile( - unicode:characters_to_list(Path), Lua)). + unicode:characters_to_list(Path), Lua)). + eval(Lua, Code) -> to_gleam(luerl:do( - unicode:characters_to_list(Code), Lua)). + unicode:characters_to_list(Code), Lua)). -eval_dec(Lua, Code) -> - to_gleam(luerl:do_dec( - unicode:characters_to_list(Code), Lua)). eval_chunk(Lua, Chunk) -> to_gleam(luerl:call_chunk(Chunk, Lua)). -eval_chunk_dec(Lua, Chunk) -> - call_function_dec(Lua, Chunk, []). eval_file(Lua, Path) -> to_gleam(luerl:dofile( - unicode:characters_to_list(Path), Lua)). + unicode:characters_to_list(Path), Lua)). -eval_file_dec(Lua, Path) -> - to_gleam(luerl:dofile_dec( - unicode:characters_to_list(Path), Lua)). call_function(Lua, Fun, Args) -> - {EncodedArgs, State} = luerl:encode_list(Args, Lua), - to_gleam(luerl:call(Fun, EncodedArgs, State)). - -call_function_dec(Lua, Fun, Args) -> - {EncodedArgs, St1} = luerl:encode_list(Args, Lua), - case luerl:call(Fun, EncodedArgs, St1) of - {ok, Ret, St2} -> - Values = luerl:decode_list(Ret, St2), - {ok, {St2, Values}}; - Other -> - to_gleam(Other) - end. + to_gleam(luerl:call(Fun, Args, Lua)). + get_private(Lua, Key) -> try diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..b098e50 --- /dev/null +++ b/test.lua @@ -0,0 +1 @@ +print(type({})) diff --git a/test/deserialize_test.gleam b/test/deserialize_test.gleam new file mode 100644 index 0000000..7b7c0cc --- /dev/null +++ b/test/deserialize_test.gleam @@ -0,0 +1,423 @@ +import deser.{DeserializeError} +import gleam/bool +import gleam/dict +import gleam/dynamic +import gleam/list +import glua + +@external(erlang, "glua_ffi", "coerce") +fn coerce_dynamic(a: anything) -> dynamic.Dynamic + +pub fn deserializer_test() { + let lua = glua.new() + let assert Ok("Hello") = deser.run(lua, glua.string("Hello"), deser.string) + + let assert Ok(42.0) = deser.run(lua, glua.int(42), deser.number) + + let assert Ok(False) = deser.run(lua, glua.bool(False), deser.bool) + + let data = ["this", "is", "some", "random", "data"] + let #(lua, ref) = data |> glua.userdata(lua, _) + let assert Ok(userdefined) = deser.run(lua, ref, deser.userdata) + assert userdefined == coerce_dynamic(data) +} + +pub fn field_ok_test() { + let lua = glua.new() + let #(lua, data) = + glua.table(lua, [ + #(glua.string("red herring"), glua.string("not here!")), + #(glua.string("name"), glua.string("Hina")), + ]) + let assert Ok(val) = + deser.run(lua, data, { + use str <- deser.field(glua.string("name"), deser.string) + deser.success(str) + }) + assert val == "Hina" +} + +pub fn field_err_test() { + let lua = glua.new() + let #(lua, data) = + glua.table(lua, [#(glua.string("red herring"), glua.string("nope"))]) + let assert Error([DeserializeError("Field", "Nothing", path)]) = + deser.run(lua, data, { + use str <- deser.field(glua.string("name"), deser.string) + deser.success(str) + }) + assert path == [glua.string("name")] +} + +pub fn subfield_ok_test() { + let lua = glua.new() + let #(lua, inner) = + glua.table(lua, [ + #(glua.int(1), glua.string("Puffy")), + #(glua.int(2), glua.string("Lucy")), + ]) + let #(lua, data) = + glua.table(lua, [ + #(glua.string("name"), glua.string("Hina")), + #(glua.string("friends"), inner), + ]) + let assert Ok(val) = + deser.run(lua, data, { + use first <- deser.subfield( + [glua.string("friends"), glua.int(1)], + deser.string, + ) + deser.success(first) + }) + assert val == "Puffy" +} + +pub fn subfield_err_test() { + let lua = glua.new() + let #(lua, inner) = + glua.table(lua, [ + #(glua.int(4), glua.string("Puffy")), + #(glua.int(2), glua.string("Lucy")), + ]) + let #(lua, data) = + glua.table(lua, [ + #(glua.string("name"), glua.string("Hina")), + #(glua.string("friends"), inner), + ]) + let assert Error([DeserializeError("Field", "Nothing", path)]) = + deser.run(lua, data, { + use first <- deser.subfield( + [glua.string("friends"), glua.int(1)], + deser.string, + ) + deser.success(first) + }) + assert path == [glua.string("friends"), glua.int(1)] +} + +pub fn field_metatable_test() { + let lua = glua.new() + let #(lua, data) = glua.table(lua, []) + let func = + glua.function(fn(lua, _args) { Ok(#(lua, [glua.string("pong")])) }) + |> glua.func_to_val + let #(lua, metatable) = + glua.table(lua, [ + #(glua.string("__index"), func), + ]) + let assert Ok(#(lua, [table])) = + glua.call_function_by_name(lua, ["setmetatable"], [data, metatable]) + let assert Ok(val) = + deser.run(lua, table, { + use pong <- deser.field( + glua.string("aasdlkjghasddlkjghasddklgjh;ksjdh"), + deser.string, + ) + deser.success(pong) + }) + assert val == "pong" +} + +pub fn at_ok_test() { + let lua = glua.new() + let #(lua, third) = + glua.table(lua, [#(glua.string("third"), glua.string("hi"))]) + let #(lua, second) = glua.table(lua, [#(glua.string("second"), third)]) + let #(lua, first) = glua.table(lua, [#(glua.string("first"), second)]) + + let third = + deser.at( + [glua.string("first"), glua.string("second"), glua.string("third")], + deser.string, + ) + let assert Ok(hi) = deser.run(lua, first, third) + assert hi == "hi" +} + +pub fn at_err_test() { + let lua = glua.new() + let #(lua, third) = + glua.table(lua, [#(glua.string("third"), glua.string("hi"))]) + let #(lua, second) = glua.table(lua, [#(glua.string("second"), third)]) + let #(lua, first) = glua.table(lua, [#(glua.string("first"), second)]) + + let third = + deser.at( + [ + glua.string("first"), + glua.string("third"), + glua.string("second"), + ], + deser.string, + ) + let assert Error([DeserializeError("Field", "Nothing", path)]) = + deser.run(lua, first, third) + assert path == ["first", "third"] |> list.map(glua.string) +} + +pub fn optionally_at_ok_test() { + let lua = glua.new() + let #(lua, nest) = + glua.table(lua, [#(glua.string("ping"), glua.string("pong"))]) + let #(lua, table) = glua.table(lua, [#(glua.string("nested"), nest)]) + let assert Ok(pong) = + deser.run( + lua, + table, + deser.optionally_at( + [glua.string("nested"), glua.string("ping")], + "miss", + deser.string, + ), + ) + assert "pong" == pong + let assert Ok(miss) = + deser.run( + lua, + table, + deser.optionally_at( + [glua.string("nestedd"), glua.string("ping")], + "miss", + deser.string, + ), + ) + assert "miss" == miss +} + +pub fn optional_field_ok_test() { + let lua = glua.new() + let #(lua, table) = + glua.table(lua, [#(glua.string("bullseye"), glua.string("hit"))]) + let assert Ok(hit) = + deser.run(lua, table, { + use hit <- deser.optional_field( + glua.string("bullseye"), + "doh i missed", + deser.string, + ) + deser.success(hit) + }) + assert hit == "hit" + let assert Ok(miss) = + deser.run(lua, table, { + use hit <- deser.optional_field( + glua.string("bull'seye"), + "doh i missed", + deser.string, + ) + deser.success(hit) + }) + + assert miss == "doh i missed" +} + +pub fn table_decode_test() { + let lua = glua.new() + let points = [#("Mark", 39.0), #("Mason", 66.0), #("Mabel", 42.0)] + let #(lua, data) = + glua.table( + lua, + points + |> list.map(fn(pair) { #(glua.string(pair.0), glua.float(pair.1)) }), + ) + let assert Ok(dict) = + deser.run(lua, data, deser.dict(deser.string, deser.number)) + assert dict == dict.from_list(points) +} + +pub fn table_list_decode_test() { + let lua = glua.new() + let meanings = [ + #(42, "the universe"), + #(39, "HATSUNE MIKU"), + #(4, "death"), + ] + let #(lua, data) = + glua.table( + lua, + meanings + |> list.map(fn(pair) { #(glua.int(pair.0), glua.string(pair.1)) }), + ) + let assert Ok(dict) = + deser.run(lua, data, deser.dict(deser.int, deser.string)) + assert dict == dict.from_list(meanings) +} + +pub fn table_list_ok_test() { + let lua = glua.new() + let greetings = [ + "Hi there", + "Hello there", + "Hey!", + "Hi everyone", + "Hello all", + "Good morning everyone", + ] + let #(lua, data) = + glua.table_list( + lua, + greetings + |> list.map(glua.string), + ) + let assert Ok(list) = deser.run(lua, data, deser.list(deser.string)) + assert list == greetings + + let #(lua, data) = glua.table(lua, []) + let assert Ok([]) = deser.run(lua, data, deser.list(deser.string)) +} + +pub fn table_list_err_test() { + let lua = glua.new() + let tf = fn(pair: #(Int, String)) { #(glua.int(pair.0), glua.string(pair.1)) } + let not_table_list = Error([DeserializeError("TableList", "Table", [])]) + + // Missing start + let #(lua, data) = + glua.table(lua, [#(2, "a"), #(3, "b"), #(4, "c")] |> list.map(tf)) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list + + // Early start + let #(lua, data) = + glua.table(lua, [#(0, "a"), #(1, "b"), #(2, "c")] |> list.map(tf)) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list + + // Gap + let #(lua, data) = + glua.table(lua, [#(1, "a"), #(2, "b"), #(4, "c")] |> list.map(tf)) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list + + // non number key + let #(lua, data) = + glua.table( + lua, + [#(1, "a"), #(2, "b"), #(3, "c")] + |> list.map(tf) + |> list.prepend(#(glua.string("wrench"), glua.string("in this table"))), + ) + assert deser.run(lua, data, deser.list(deser.string)) == not_table_list +} + +pub fn then_test() { + let positive_deser = { + use _lua, num <- deser.then(deser.number) + case num >. 0.0 { + False -> deser.failure(0.0, "PositiveNum") + True -> deser.success(num) + } + } + let lua = glua.new() + let assert Ok(4.0) = deser.run(lua, glua.int(4), positive_deser) + let assert Error([DeserializeError("PositiveNum", "Int", [])]) = + deser.run(lua, glua.int(-4), positive_deser) +} + +type Person { + Person(name: String, age: Int) +} + +pub fn then_state_test() { + let person_deser = { + use lua, table <- deser.then(deser.raw) + let assert Ok(#(lua, [meta])) = + glua.call_function_by_name(lua, ["getmetatable"], [table]) + let is_person = case + deser.run(lua, meta, deser.at([glua.string("type")], deser.string)) + { + Ok(kind) -> kind == "person" + Error(_) -> False + } + use <- bool.guard(!is_person, deser.failure(Person("", 0), "Person")) + use name <- deser.field(glua.string("name"), deser.string) + use age <- deser.field(glua.string("age"), deser.int) + deser.success(Person(name, age)) + } + + let lua = glua.new() + let #(lua, data) = + glua.table(lua, [ + #(glua.string("name"), glua.string("Captain Falcon")), + #(glua.string("age"), glua.int(36)), + ]) + let #(lua, metatable) = + glua.table(lua, [#(glua.string("type"), glua.string("person"))]) + let assert Ok(#(lua, _)) = + glua.call_function_by_name(lua, ["setmetatable"], [data, metatable]) + let assert Ok(person) = deser.run(lua, data, person_deser) + assert Person("Captain Falcon", 36) == person +} + +pub fn custom_function_ok_test() { + let assert Ok(#(lua, [func])) = + glua.eval(glua.new(), "return function () return 42 end") + let assert Ok(func) = deser.run(lua, func, deser.function) + let assert Ok(#(lua, [num])) = glua.call_function(lua, func, []) + let assert Ok(42.0) = deser.run(lua, num, deser.number) +} + +pub fn builtin_function_ok_test() { + let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return string.upper") + let assert Ok(func) = deser.run(lua, func, deser.function) + let assert Ok(#(lua, [str])) = + glua.call_function(lua, func, [glua.string("hello")]) + let assert Ok("HELLO") = deser.run(lua, str, deser.string) +} + +pub fn function_err_test() { + let assert Ok(#(lua, [func])) = glua.eval(glua.new(), "return string") + let assert Error([DeserializeError("Function", "Table", [])]) = + deser.run(lua, func, deser.function) +} + +pub fn nonexistent_table_test() { + let #(_lua, ref) = + glua.table(glua.new(), [#(glua.string("key"), glua.string("value"))]) + let assert Error([DeserializeError("Table", "NonexistentTable", [])]) = + deser.run(glua.new(), ref, deser.dict(deser.string, deser.string)) +} + +pub fn nonexistent_userdata_test() { + let #(_lua, ref) = glua.userdata(glua.new(), 3) + let assert Error([DeserializeError("UserData", "NonexistentUserData", [])]) = + deser.run(glua.new(), ref, deser.userdata) +} + +pub fn arg_ok_test() { + let args = [glua.string("Hello"), glua.int(42)] + let lua = glua.new() + let assert Ok(#("Hello", 42)) = + deser.run_multi(lua, args, { + use hello <- deser.item(1, deser.string) + use num <- deser.item(2, deser.int) + deser.success(#(hello, num)) + }) + Nil +} + +pub fn arg_error_test() { + let lua = glua.new() + let #(lua, table) = glua.table(lua, []) + let args = [table, glua.int(42)] + + let assert Error([err]) = + deser.run_multi(lua, args, { + use hello <- deser.item(1, { + use it <- deser.field(glua.string("nest"), { + use it <- deser.field(glua.string("another nest"), deser.int) + deser.success(it) + }) + deser.success(it) + }) + use num <- deser.item(2, deser.int) + deser.success(#(hello, num)) + }) + assert err + == DeserializeError( + "Field", + "Nothing", + [ + "", + "nest", + ] + |> list.map(glua.string), + ) +} diff --git a/test/glua_test.gleam b/test/glua_test.gleam index 184baa2..8af4e50 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -1,11 +1,13 @@ +import deser +import gleam/bit_array 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 gleeunit + import glua pub fn main() -> Nil { @@ -15,30 +17,31 @@ pub fn main() -> Nil { pub fn get_table_test() { let lua = glua.new() let my_table = [ - #("meaning of life", 42), - #("pi", 3), - #("euler's number", 3), + #("meaning of life", 42.0), + #("pi", 3.0), + #("euler's number", 3.0), ] + let cool_numbers = glua.function(fn(lua, _params) { - let table = + let #(lua, table) = glua.table( + lua, my_table - |> list.map(fn(pair) { #(glua.string(pair.0), glua.int(pair.1)) }), + |> list.map(fn(pair) { #(glua.string(pair.0), glua.float(pair.1)) }), ) - #(lua, [table]) + Ok(#(lua, [table])) }) + |> glua.func_to_val let assert Ok(lua) = glua.set(lua, ["cool_numbers"], cool_numbers) - let assert Ok(#(_lua, [table])) = - glua.call_function_by_name( - lua, - ["cool_numbers"], - [], - using: glua.table_decoder(decode.string, decode.int), - ) + let assert Ok(#(lua, [table])) = + glua.call_function_by_name(lua, ["cool_numbers"], []) + + let assert Ok(table) = + deser.run(lua, table, deser.dict(deser.string, deser.number)) - assert dict.from_list(table) == dict.from_list(my_table) + assert table == dict.from_list(my_table) } pub fn sandbox_test() { @@ -46,30 +49,21 @@ pub fn sandbox_test() { let args = list.map([20, 10], glua.int) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name( - state: lua, - keys: ["math", "max"], - args:, - using: decode.int, - ) + glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) assert exception == glua.ErrorCall(["math.max is sandboxed"]) let assert Ok(lua) = glua.sandbox(glua.new(), ["string"]) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = - glua.eval( - state: lua, - code: "return string.upper('my_string')", - using: decode.string, - ) + glua.eval(state: lua, code: "return string.upper('my_string')") assert name == "upper" let assert Ok(lua) = glua.sandbox(glua.new(), ["os", "execute"]) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.ref_eval( + glua.eval( state: lua, code: "os.execute(\"echo 'sandbox test is failing'\"); os.exit(1)", ) @@ -79,12 +73,7 @@ pub fn sandbox_test() { let assert Ok(lua) = glua.sandbox(glua.new(), ["print"]) let arg = glua.string("sandbox test is failing") let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.call_function_by_name( - state: lua, - keys: ["print"], - args: [arg], - using: decode.string, - ) + glua.call_function_by_name(state: lua, keys: ["print"], args: [arg]) assert exception == glua.ErrorCall(["print is sandboxed"]) } @@ -93,18 +82,18 @@ pub fn new_sandboxed_test() { let assert Ok(lua) = glua.new_sandboxed([]) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.ref_eval(state: lua, code: "return load(\"return 1\")") + glua.eval(state: lua, code: "return load(\"return 1\")") assert exception == glua.ErrorCall(["load is sandboxed"]) let arg = glua.int(1) let assert Error(glua.LuaRuntimeException(exception, _)) = - glua.ref_call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) + glua.call_function_by_name(state: lua, keys: ["os", "exit"], args: [arg]) assert exception == glua.ErrorCall(["os.exit is sandboxed"]) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(_, name), _)) = - glua.ref_eval(state: lua, code: "io.write('some_message')") + glua.eval(state: lua, code: "io.write('some_message')") assert name == "write" @@ -112,43 +101,9 @@ pub fn new_sandboxed_test() { let assert Ok(lua) = glua.set_lua_paths(lua, paths: ["./test/lua/?.lua"]) let code = "local s = require 'example'; return s" - let assert Ok(#(_, [result])) = - glua.eval(state: lua, code:, using: decode.string) - - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" -} - -pub fn encoding_and_decoding_nested_tables_test() { - let nested_table = [ - #( - glua.string("key"), - glua.table([ - #( - glua.int(1), - glua.table([#(glua.string("deeper_key"), glua.string("deeper_value"))]), - ), - ]), - ), - ] - - let keys = ["my_nested_table"] - - let nested_table_decoder = - glua.table_decoder( - decode.string, - glua.table_decoder( - decode.int, - glua.table_decoder(decode.string, decode.string), - ), - ) - let tbl = glua.table(nested_table) - - let assert Ok(lua) = glua.set(state: glua.new(), keys:, value: tbl) + let assert Ok(#(_, [result])) = glua.eval(state: lua, code:) - let assert Ok(result) = - glua.get(state: lua, keys:, using: nested_table_decoder) - - assert result == [#("key", [#(1, [#("deeper_key", "deeper_value")])])] + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") } pub type Userdata { @@ -164,60 +119,61 @@ pub fn userdata_test() { decode.success(Userdata(foo:, bar:)) } - let assert Ok(lua) = glua.set(lua, ["my_userdata"], glua.userdata(userdata)) - let assert Ok(#(lua, [result])) = - glua.eval(lua, "return my_userdata", userdata_decoder) + let #(lua, data) = glua.userdata(lua, userdata) + let assert Ok(lua) = glua.set(lua, ["my_userdata"], data) + let assert Ok(#(lua, [result])) = glua.eval(lua, "return my_userdata") + let assert Ok(result) = deser.run(lua, result, deser.userdata) + let assert Ok(result) = decode.run(result, userdata_decoder) assert result == userdata let userdata = Userdata("other_userdata", 2) - let assert Ok(lua) = - glua.set(lua, ["my_other_userdata"], glua.userdata(userdata)) + let #(lua, data) = glua.userdata(lua, userdata) + let assert Ok(lua) = glua.set(lua, ["my_other_userdata"], data) let assert Error(glua.LuaRuntimeException(glua.IllegalIndex(value, index), _)) = - glua.eval(lua, "return my_other_userdata.foo", decode.string) + glua.eval(lua, "return my_other_userdata.foo") assert value == "{usdref,1}" assert index == "foo" } pub fn get_test() { - let state = glua.new() + let lua = glua.new() - let assert Ok(pi) = - glua.get(state: state, keys: ["math", "pi"], using: decode.float) + let assert Ok(pi) = glua.get(state: lua, keys: ["math", "pi"]) + let assert Ok(pi) = deser.run(lua, pi, deser.number) assert pi >. 3.14 && pi <. 3.15 let keys = ["my_table", "my_value"] let encoded = glua.bool(True) - let assert Ok(state) = glua.set(state:, keys:, value: encoded) - let assert Ok(ret) = glua.get(state:, keys:, using: decode.bool) + let assert Ok(lua) = glua.set(state: lua, keys:, value: encoded) + let assert Ok(ret) = glua.get(state: lua, keys:) - assert ret == True + assert ret == glua.bool(True) let code = " my_value = 10 return 'ignored' " - let assert Ok(#(state, _)) = - glua.new() |> glua.eval(code:, using: decode.string) - let assert Ok(ret) = glua.get(state:, keys: ["my_value"], using: decode.int) + let assert Ok(#(lua, _)) = glua.new() |> glua.eval(code:) + let assert Ok(ret) = glua.get(state: lua, keys: ["my_value"]) - assert ret == 10 + assert ret == glua.int(10) } pub fn get_returns_proper_errors_test() { let state = glua.new() - assert glua.get(state:, keys: ["non_existent_global"], using: decode.string) + assert glua.get(state:, keys: ["non_existent_global"]) == Error(glua.KeyNotFound) let encoded = glua.int(10) let assert Ok(state) = glua.set(state:, keys: ["my_table", "some_value"], value: encoded) - assert glua.get(state:, keys: ["my_table", "my_val"], using: decode.int) + assert glua.get(state:, keys: ["my_table", "my_val"]) == Error(glua.KeyNotFound) } @@ -226,8 +182,8 @@ pub fn set_test() { let assert Ok(lua) = glua.set(state: glua.new(), keys: ["_VERSION"], value: encoded) - let assert Ok(result) = - glua.get(state: lua, keys: ["_VERSION"], using: decode.string) + let assert Ok(result) = glua.get(state: lua, keys: ["_VERSION"]) + let assert Ok(result) = deser.run(lua, result, deser.string) assert result == "custom version" @@ -237,64 +193,58 @@ pub fn set_test() { let keys = ["math", "squares"] - let encoded = + let #(lua, encoded) = glua.table( + lua, numbers |> list.map(fn(pair) { #(glua.int(pair.0), glua.int(pair.1)) }), ) let assert Ok(lua) = glua.set(lua, keys, encoded) - assert glua.get(lua, keys, glua.table_decoder(decode.int, decode.int)) - == Ok([#(1, 4), #(2, 16), #(3, 49), #(4, 144)]) + let assert Ok(val) = glua.get(lua, keys) + let assert Ok(val) = deser.run(lua, val, deser.list(deser.int)) + assert val == [4, 16, 49, 144] - let count_odd = fn(lua: glua.Lua, args: List(dynamic.Dynamic)) { + let count_odd = fn(lua: glua.Lua, args: List(glua.Value)) { let assert [list] = args - let assert Ok(list) = - decode.run(list, glua.table_decoder(decode.int, decode.int)) - - let count = - list.map(list, pair.second) - |> list.count(int.is_odd) + let assert Ok(list) = deser.run(lua, list, deser.list(deser.int)) - #(lua, list.map([count], glua.int)) + let count = list.count(list, int.is_odd) + Ok(#(lua, list.map([count], glua.int))) } - let encoded = glua.function(count_odd) - let assert Ok(lua) = glua.set(glua.new(), ["count_odd"], encoded) + let encoded = glua.function(count_odd) |> glua.func_to_val + let assert Ok(lua) = glua.set(lua, ["count_odd"], encoded) - let arg = + let #(lua, arg) = glua.table( - list.index_map(list.range(1, 10), fn(i, n) { - #(glua.int(i + 1), glua.int(n)) - }), + lua, + list.index_map(list.range(1, 10), fn(i, n) { #(glua.int(i), glua.int(n)) }), ) let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["count_odd"], - args: [arg], - using: decode.int, - ) + glua.call_function_by_name(state: lua, keys: ["count_odd"], args: [arg]) - assert result == 5 + assert result == glua.int(5) - let tbl = - glua.table([ + let #(lua, tbl) = + glua.table(lua, [ #( glua.string("is_even"), glua.function(fn(lua, args) { let assert [arg] = args - let assert Ok(arg) = decode.run(arg, decode.int) - #(lua, list.map([int.is_even(arg)], glua.bool)) - }), + let assert Ok(arg) = deser.run(lua, arg, deser.int) + Ok(#(lua, list.map([int.is_even(arg)], glua.bool))) + }) + |> glua.func_to_val, ), #( glua.string("is_odd"), glua.function(fn(lua, args) { let assert [arg] = args - let assert Ok(arg) = decode.run(arg, decode.int) - #(lua, list.map([int.is_odd(arg)], glua.bool)) - }), + let assert Ok(arg) = deser.run(lua, arg, deser.int) + Ok(#(lua, list.map([int.is_odd(arg)], glua.bool))) + }) + |> glua.func_to_val, ), ]) @@ -307,19 +257,14 @@ pub fn set_test() { state: lua, keys: ["my_functions", "is_even"], args: [arg], - using: decode.bool, ) - assert result == True + assert result == glua.bool(True) let assert Ok(#(_, [result])) = - glua.eval( - state: lua, - code: "return my_functions.is_odd(4)", - using: decode.bool, - ) + glua.eval(state: lua, code: "return my_functions.is_odd(4)") - assert result == False + assert result == glua.bool(False) } pub fn set_lua_paths_test() { @@ -328,84 +273,73 @@ pub fn set_lua_paths_test() { let code = "local s = require 'example'; return s" - let assert Ok(#(_, [result])) = glua.eval(state:, code:, using: decode.string) + let assert Ok(#(_, [result])) = glua.eval(state:, code:) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") } pub fn get_private_test() { - assert glua.new() - |> glua.set_private("test", [1, 2, 3]) - |> glua.get_private("test", using: decode.list(decode.int)) - == Ok([1, 2, 3]) + let lua = glua.new() |> glua.set_private("test", [1, 2, 3]) + let assert Ok(priv) = glua.get_private(lua, "test") + assert decode.run(priv, decode.list(decode.int)) == Ok([1, 2, 3]) assert glua.new() - |> glua.get_private("non_existent", using: decode.string) + |> glua.get_private("non_existent") == Error(glua.KeyNotFound) } pub fn delete_private_test() { let lua = glua.set_private(glua.new(), "the_value", "that_will_be_deleted") - assert glua.get_private(lua, "the_value", using: decode.string) - == Ok("that_will_be_deleted") + let assert Ok(priv) = glua.get_private(lua, "the_value") + assert decode.run(priv, decode.string) == Ok("that_will_be_deleted") - assert glua.delete_private(lua, "the_value") - |> glua.get_private(key: "the_value", using: decode.string) - == Error(glua.KeyNotFound) + let lua = glua.delete_private(lua, "the_value") + let assert Error(glua.KeyNotFound) = + glua.get_private(state: lua, key: "the_value") } pub fn load_test() { let assert Ok(#(lua, chunk)) = glua.load(state: glua.new(), code: "return 5 * 5") - let assert Ok(#(_, [result])) = - glua.eval_chunk(state: lua, chunk:, using: decode.int) + let assert Ok(#(_, [result])) = glua.eval_chunk(state: lua, chunk:) - assert result == 25 + assert result == glua.int(25) } pub fn eval_load_file_test() { let assert Ok(#(lua, chunk)) = glua.load_file(state: glua.new(), path: "./test/lua/example.lua") - let assert Ok(#(_, [result])) = - glua.eval_chunk(state: lua, chunk:, using: decode.string) + let assert Ok(#(_, [result])) = glua.eval_chunk(state: lua, chunk:) - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") } pub fn eval_test() { let assert Ok(#(lua, [result])) = - glua.eval( - state: glua.new(), - code: "return 'hello, ' .. 'world!'", - using: decode.string, - ) + glua.eval(state: glua.new(), code: "return 'hello, ' .. 'world!'") - assert result == "hello, world!" + assert result == glua.string("hello, world!") - let assert Ok(#(_, results)) = - glua.eval(state: lua, code: "return 2 + 2, 3 - 1", using: decode.int) + let assert Ok(#(_, [a, b])) = + glua.eval(state: lua, code: "return 2 + 2, 3 - 1") - assert results == [4, 2] + assert a == glua.int(4) + assert b == glua.int(2) } pub fn eval_returns_proper_errors_test() { let state = glua.new() - assert glua.eval(state:, code: "if true then 1 + ", using: decode.int) + assert glua.eval(state:, code: "if true then 1 + ") == Error( glua.LuaCompilerException(messages: ["syntax error before: ", "1"]), ) - assert glua.eval(state:, code: "return 'Hello from Lua!'", using: decode.int) - == Error( - glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), - ) - let assert Error(glua.LuaRuntimeException( exception: glua.IllegalIndex(value:, index:), state: _, - )) = glua.eval(state:, code: "return a.b", using: decode.int) + )) = glua.eval(state:, code: "return a.b") assert value == "nil" assert index == "b" @@ -413,20 +347,20 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.ErrorCall(messages:), state: _, - )) = glua.eval(state:, code: "error('error message')", using: decode.int) + )) = glua.eval(state:, code: "error('error message')") assert messages == ["error message"] let assert Error(glua.LuaRuntimeException( exception: glua.UndefinedFunction(value:), state: _, - )) = glua.eval(state:, code: "local a = 5; a()", using: decode.int) + )) = glua.eval(state:, code: "local a = 5; a()") assert value == "5" let assert Error(glua.LuaRuntimeException( exception: glua.BadArith(operator:, args:), state: _, - )) = glua.eval(state:, code: "return 10 / 0", using: decode.int) + )) = glua.eval(state:, code: "return 10 / 0") assert operator == "/" assert args == ["10", "0"] @@ -434,107 +368,58 @@ pub fn eval_returns_proper_errors_test() { let assert Error(glua.LuaRuntimeException( exception: glua.AssertError(message:), state: _, - )) = - glua.eval( - state:, - code: "assert(1 == 2, 'assertion failed')", - using: decode.int, - ) + )) = glua.eval(state:, code: "assert(1 == 2, 'assertion failed')") assert message == "assertion failed" } pub fn eval_file_test() { let assert Ok(#(_, [result])) = - glua.eval_file( - state: glua.new(), - path: "./test/lua/example.lua", - using: decode.string, - ) + glua.eval_file(state: glua.new(), path: "./test/lua/example.lua") - assert result == "LUA IS AN EMBEDDABLE LANGUAGE" + assert result == glua.string("LUA IS AN EMBEDDABLE LANGUAGE") } pub fn call_function_test() { let assert Ok(#(lua, [fun])) = - glua.ref_eval(state: glua.new(), code: "return string.reverse") + glua.eval(state: glua.new(), code: "return string.reverse") let encoded = glua.string("auL") + let assert Ok(fun) = deser.run(lua, fun, deser.function) let assert Ok(#(lua, [result])) = - glua.call_function( - state: lua, - ref: fun, - args: [encoded], - using: decode.string, - ) + glua.call_function(state: lua, fun: fun, args: [encoded]) - assert result == "Lua" + assert result == glua.string("Lua") let assert Ok(#(lua, [fun])) = - glua.ref_eval(state: lua, code: "return function(a, b) return a .. b end") + glua.eval(state: lua, code: "return function(a, b) return a .. b end") + let assert Ok(fun) = deser.run(lua, fun, deser.function) let args = list.map(["Lua in ", "Gleam"], glua.string) let assert Ok(#(_, [result])) = - glua.call_function(state: lua, ref: fun, args:, using: decode.string) + glua.call_function(state: lua, fun: fun, args:) - assert result == "Lua in Gleam" -} - -pub fn call_function_returns_proper_errors_test() { - let state = glua.new() - - let assert Ok(#(state, [ref])) = - glua.ref_eval(state:, code: "return string.upper") - - let arg = glua.string("Hello from Gleam!") - - assert glua.call_function(state:, ref:, args: [arg], using: decode.int) - == Error( - glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), - ) - - let assert Ok(#(lua, [ref])) = glua.ref_eval(state:, code: "return 1") - - let assert Error(glua.LuaRuntimeException( - exception: glua.UndefinedFunction(value:), - state: _, - )) = glua.call_function(state: lua, ref:, args: [], using: decode.string) - - assert value == "1" + assert result == glua.string("Lua in Gleam") } pub fn call_function_by_name_test() { let args = list.map([20, 10], glua.int) let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: glua.new(), - keys: ["math", "max"], - args:, - using: decode.int, - ) + glua.call_function_by_name(state: glua.new(), keys: ["math", "max"], args:) - assert result == 20 + assert result == glua.int(20) let assert Ok(#(lua, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["math", "min"], - args:, - using: decode.int, - ) + glua.call_function_by_name(state: lua, keys: ["math", "min"], args:) - assert result == 10 + assert result == glua.int(10) let arg = glua.float(10.2) - let assert Ok(#(_, [result])) = - glua.call_function_by_name( - state: lua, - keys: ["math", "type"], - args: [arg], - using: decode.optional(decode.string), - ) + let assert Ok(#(lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "type"], args: [arg]) + let assert Ok(result) = deser.run(lua, result, deser.optional(deser.string)) assert result == option.Some("float") } @@ -542,12 +427,79 @@ pub fn call_function_by_name_test() { pub fn nested_function_references_test() { let code = "return function() return math.sqrt end" - let assert Ok(#(lua, [ref])) = glua.ref_eval(state: glua.new(), code:) - let assert Ok(#(lua, [ref])) = - glua.ref_call_function(state: lua, ref:, args: []) + let assert Ok(#(lua, [ref])) = glua.eval(state: glua.new(), code:) + let assert Ok(fun) = deser.run(lua, ref, deser.function) + let assert Ok(#(lua, [ref])) = glua.call_function(state: lua, fun:, args: []) + let assert Ok(fun) = deser.run(lua, ref, deser.function) let arg = glua.int(400) let assert Ok(#(_, [result])) = - glua.call_function(state: lua, ref:, args: [arg], using: decode.float) - assert result == 20.0 + glua.call_function(state: lua, fun:, args: [arg]) + assert result == glua.float(20.0) +} + +pub fn alloc_test() { + let #(lua, table) = glua.table(glua.new(), []) + let proxy = + glua.function(fn(lua, _args) { Ok(#(lua, [glua.string("constant")])) }) + |> glua.func_to_val + let #(lua, metatable) = glua.table(lua, [#(glua.string("__index"), proxy)]) + let assert Ok(#(lua, _)) = + glua.call_function_by_name(lua, ["setmetatable"], [table, metatable]) + let assert Ok(lua) = glua.set(lua, ["test_table"], table) + + let assert Ok(#(_lua, [ret1])) = glua.eval(lua, "return test_table.any_key") + + let assert Ok(#(_lua, [ret2])) = glua.eval(lua, "return test_table.other_key") + + assert ret1 == glua.string("constant") + assert ret2 == glua.string("constant") +} + +fn bad_arg_error(lua: glua.Lua, errors: List(deser.DeserializeError)) { + #(lua, errors |> list.map(deser.error_to_string)) +} + +pub fn throw_error_test() { + let add_func = + glua.function(fn(lua, params) { + let result = + deser.run_multi(lua, params, { + use augend <- deser.item(1, deser.number) + use addend <- deser.item(2, deser.number) + deser.success(#(augend, addend)) + }) + |> result.map_error(bad_arg_error(lua, _)) + use #(augend, addend) <- result.try(result) + Ok(#(lua, [glua.float(augend +. addend)])) + }) + let lua = glua.new() + let assert Ok(#(lua, [result])) = + glua.call_function(lua, add_func, [glua.int(2), glua.int(4)]) + let assert Ok(6.0) = deser.run(lua, result, deser.number) + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall([ + "Expected Field got Nothing at [\"\"]", + "Expected Field got Nothing at [\"\"]", + ]), + state: _lua, + )) = glua.call_function(lua, add_func, []) + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall([ + "Expected Number got String at [\"\"]", + "Expected Field got Nothing at [\"\"]", + ]), + state: _lua, + )) = glua.call_function(lua, add_func, [glua.string("1")]) +} + +pub fn non_utf8_test() { + let lua = glua.new() + let assert Ok(chars) = glua.get(lua, ["_G", "utf8", "charpattern"]) + let assert Error([deser.DeserializeError("String", "ByteString", [])]) = + deser.run(lua, chars, deser.string) + + let assert Ok(str) = deser.run(lua, chars, deser.byte_string) + assert !bit_array.is_utf8(str) } diff --git a/test/lua/two_numbers.lua b/test/lua/two_numbers.lua new file mode 100644 index 0000000..cd12910 --- /dev/null +++ b/test/lua/two_numbers.lua @@ -0,0 +1 @@ +return 1, 2 diff --git a/test/mut_deserialize_test.gleam b/test/mut_deserialize_test.gleam new file mode 100644 index 0000000..e69de29 diff --git a/test/readme_test.gleam b/test/readme_test.gleam new file mode 100644 index 0000000..4dd14b9 --- /dev/null +++ b/test/readme_test.gleam @@ -0,0 +1,163 @@ +import deser +import gleam/dict +import gleam/float +import gleam/list +import glua + +/// This file contains all the tests in the readme +pub fn eval_test() { + let code = + " +function greet() + return 'Hello from Lua!' +end + +return greet() +" + + // Make a fresh instance lua instance + let lua = glua.new() + let assert Ok(#(_state, [result])) = glua.eval(state: lua, code:) + + assert result == glua.string("Hello from Lua!") +} + +pub type Project { + Project(name: String, language: String) +} + +pub fn deser_test() { + let code = "return { name = 'glua', written_in = 'gleam'}" + let assert Ok(#(lua, [table])) = glua.eval(glua.new(), code) + + let deserializer = { + use name <- deser.field(glua.string("name"), deser.string) + use language <- deser.field(glua.string("written_in"), deser.string) + deser.success(Project(name:, language:)) + } + + let assert Ok(project) = deser.run(lua, table, deserializer) + assert project == Project(name: "glua", language: "gleam") +} + +pub fn parse_execute_chunk() { + let code = "return 'this is a chunk of Lua code'" + let assert Ok(#(state, chunk)) = glua.load(state: glua.new(), code:) + let assert Ok(#(_state, [result])) = glua.eval_chunk(state:, chunk:) + + assert result == glua.string("this is a chunk of Lua code") +} + +pub fn exec_lua_files_test() { + let assert Ok(#(_state, [n, m])) = + glua.eval_file(state: glua.new(), path: "./test/lua/two_numbers.lua") + + assert n == glua.int(1) && m == glua.int(2) +} + +pub fn sandboxing_test() { + let assert Ok(lua) = glua.new() |> glua.sandbox(["os", "execute"]) + let assert Error(glua.LuaRuntimeException(exception, _)) = + glua.eval(state: lua, code: "os.execute('rm -f important_file')") + + // 'important_file' was not deleted + assert exception == glua.ErrorCall(["os.execute is sandboxed"]) +} + +pub fn get_global_values() { + let assert Ok(version) = glua.get(state: glua.new(), keys: ["_VERSION"]) + + assert version == glua.string("Lua 5.3") +} + +pub fn set_global_values() { + let lua = glua.new() + // `keys` is the full path to where the value will be set + // and any intermediate table will be created if it is not present + let keys = ["my_table", "my_value"] + let assert Ok(lua) = + glua.set(state: lua, keys:, value: glua.string("my_value")) + + // now we can get the value + let assert Ok(value) = glua.get(state: lua, keys:) + + // or return it from a Lua script + let assert Ok(#(_lua, [returned])) = + glua.eval(state: lua, code: "return my_table.my_value") + + assert value == glua.string("my_value") + assert returned == glua.string("my_value") +} + +pub fn table_deocding_test() { + // we can also encode a list of tuples as a table to set it in Lua + let my_table = [ + #(glua.string("my_first_value"), glua.float(1.2)), + #(glua.string("my_second_value"), glua.float(2.1)), + ] + + let #(lua, encoded) = glua.table(glua.new(), my_table) + let assert Ok(lua) = glua.set(state: lua, keys: ["my_table"], value: encoded) + + // now we can get its values + let assert Ok(#(lua, [result])) = + glua.eval(state: lua, code: "return my_table.my_second_value") + + let assert Ok(2.1) = deser.run(lua, result, deser.number) + // or we can get the whole table and decode it back to a list of tuples + let assert Ok(table) = glua.get(state: lua, keys: ["my_table"]) + let assert Ok(table) = + deser.run(lua, table, deser.dict(deser.string, deser.number)) + + assert table + == dict.from_list([#("my_first_value", 1.2), #("my_second_value", 2.1)]) +} + +pub fn call_functions_test() { + let lua = glua.new() + let assert Ok(val) = glua.get(state: lua, keys: ["math", "max"]) + let assert Ok(fun) = deser.run(lua, val, deser.function) + let args = [1, 20, 7, 18] |> list.map(glua.int) + + let assert Ok(#(lua, [result])) = + glua.call_function(state: lua, fun: fun, args:) + + let assert Ok(result) = deser.run(lua, result, deser.number) + + assert result == 20.0 + + // `glua.call_function_by_name` is a shorthand for `glua.ref_get` followed by `glua.call_function` + let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys: ["math", "max"], args:) + + let assert Ok(result) = deser.run(lua, result, deser.number) + + assert result == 20.0 +} + +pub fn expose_functions_test() { + let lua = glua.new() + let fun = + glua.function(fn(lua, args) { + // Since Gleam is a statically typed language, each and every argument must be decoded + let assert [x, min, max] = args + let assert Ok(x) = deser.run(lua, x, deser.number) + let assert Ok(min) = deser.run(lua, min, deser.number) + let assert Ok(max) = deser.run(lua, max, deser.number) + + let result = float.clamp(x, min, max) + + Ok(#(lua, [glua.float(result)])) + }) + |> glua.func_to_val + + let keys = ["my_functions", "clamp"] + + let assert Ok(lua) = glua.set(state: lua, keys:, value: fun) + + let args = [2.3, 1.2, 2.1] |> list.map(glua.float) + let assert Ok(#(_lua, [result])) = + glua.call_function_by_name(state: lua, keys:, args:) + + assert result == glua.float(2.1) +}