diff --git a/src/glua.gleam b/src/glua.gleam index 24c2f1e..b2b740c 100644 --- a/src/glua.gleam +++ b/src/glua.gleam @@ -1164,3 +1164,73 @@ pub fn call_function_by_name( 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, "a_key") +/// }) +/// // -> Ok(#(_state, ["fixed value"])) +/// ``` +pub fn index(table ref: Value, key key: String) -> Action(Value, e) { + Action(do_index(_, ref, key)) +} + +@external(erlang, "glua_ffi", "get_table_key") +fn do_index( + state: Lua, + ref: Value, + key: String, +) -> Result(#(Lua, Value), LuaError(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, "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: String, + 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: String, + val: Value, +) -> Result(#(Lua, Nil), LuaError(e)) diff --git a/src/glua_ffi.erl b/src/glua_ffi.erl index acf808c..77cbe48 100644 --- a/src/glua_ffi.erl +++ b/src/glua_ffi.erl @@ -3,8 +3,8 @@ -import(ttdict, [fold/3]). -include_lib("luerl/include/luerl.hrl"). --export([get_stacktrace/1, dereference/2, coerce/1, coerce_nil/0, wrap_fun/1, decode_fun/1, sandbox_fun/1, get_table_keys/2, - get_private/2, set_table_keys/3, load/2, load_file/2, eval/2, eval_file/2, +-export([get_stacktrace/1, dereference/2, coerce/1, coerce_nil/0, wrap_fun/1, decode_fun/1, sandbox_fun/1, get_table_key/3, + get_table_keys/2, get_private/2, set_table_key/4, set_table_keys/3, load/2, load_file/2, eval/2, eval_file/2, eval_chunk/2, call_function/3]). @@ -238,6 +238,14 @@ decode_fun(Fun) -> _ -> {error, fun(_) -> {action, fun(State) -> {ok, {State, nil}} end} end} end. +get_table_key(Lua, Tref, Key) -> + try luerl_emul:get_table_key(Tref, Key, Lua) of + {nil, _St} -> {error, {key_not_found, [Key]}}; + {Value, St} -> {ok, {St, Value}} + catch + error:{lua_error, _, _} = Err -> {error, map_error(Err)} + end. + get_table_keys(Lua, Keys) -> case luerl:get_table_keys(Keys, Lua) of {ok, nil, _} -> @@ -248,6 +256,14 @@ get_table_keys(Lua, Keys) -> to_gleam(Other) end. +set_table_key(Lua, Tref, Key, Value) -> + try + St = luerl_emul:set_table_key(Tref, Key, Value, Lua), + {ok, {St, nil}} + catch + error:{lua_error, _, _} = Err -> {error, map_error(Err)} + end. + set_table_keys(Lua, Keys, Value) -> to_gleam(luerl:set_table_keys(Keys, Value, Lua)). diff --git a/test/glua_test.gleam b/test/glua_test.gleam index d28b2ae..13e1c5c 100644 --- a/test/glua_test.gleam +++ b/test/glua_test.gleam @@ -8,6 +8,7 @@ import gleam/pair import gleam/result import gleeunit import glua +import glua/lib pub fn main() -> Nil { gleeunit.main() @@ -906,3 +907,144 @@ pub fn decode_function_test() { assert exn == glua.BadArith("*", ["3", "true"]) } + +pub fn index_test() { + let assert Ok(#(state, tbl)) = + glua.run(glua.new(), { + glua.table([#(glua.string("a_key"), glua.string("a value"))]) + }) + + assert glua.run( + state, + glua.index(tbl, "a_key") |> glua.returning(decode.string), + ) + |> result.map(pair.second) + == Ok("a value") + + assert glua.run(state, { + use tbl <- glua.then( + glua.table([#(glua.string("a_key"), glua.string("a value"))]), + ) + glua.index(tbl, "other_key") + }) + == Error(glua.KeyNotFound(["other_key"])) + + let assert Ok(#(state, tbl)) = + glua.run(glua.new(), { + use tbl <- glua.then(glua.table([])) + use mt <- glua.then( + glua.table([ + #( + glua.string("__index"), + glua.function(fn(args) { + let assert [_table, key] = args + + use k <- glua.then(glua.dereference(key, decode.string)) + case int.parse(k) { + Ok(_) -> "integer" + Error(_) -> "other" + } + |> glua.string + |> list.wrap + |> glua.success + }), + ), + ]), + ) + + use _ <- glua.then( + glua.call_function_by_name(["setmetatable"], [tbl, mt]), + ) + glua.success(tbl) + }) + + assert glua.run(state, glua.index(tbl, "1") |> glua.returning(decode.string)) + |> result.map(pair.second) + == Ok("integer") + + assert glua.run(state, glua.index(tbl, "a") |> glua.returning(decode.string)) + |> result.map(pair.second) + == Ok("other") + + assert glua.run(state, glua.index(tbl, "2") |> glua.returning(decode.int)) + |> result.map(pair.second) + == Error( + glua.UnexpectedResultType([decode.DecodeError("Int", "String", [])]), + ) +} + +pub fn new_index_test() { + let assert Ok(#(state, tbl)) = glua.run(glua.new(), { glua.table([]) }) + + assert glua.run(state, { + use _ <- glua.then(glua.new_index(tbl, "a_key", glua.int(1))) + use ref <- glua.then(glua.index(tbl, "a_key")) + glua.dereference(ref, decode.int) + }) + |> result.map(pair.second) + == Ok(1) + + let assert Ok(#(state, tbl)) = + glua.run(glua.new(), { + use tbl <- glua.then(glua.table([])) + use mt <- glua.then( + glua.table([ + #( + glua.string("__newindex"), + glua.function(fn(args) { + let assert [table, key, value] = args + + use i <- glua.then(glua.dereference(value, decode.int)) + + let value = glua.int(i * i) + glua.call_function(lib.raw_set(), [table, key, value]) + }), + ), + ]), + ) + + use _ <- glua.then( + glua.call_function_by_name(["setmetatable"], [tbl, mt]), + ) + glua.success(tbl) + }) + + assert glua.run(state, { + use _ <- glua.then(glua.new_index(tbl, "a_key", glua.int(6))) + use ref <- glua.then(glua.index(tbl, "a_key")) + glua.dereference(ref, decode.int) + }) + |> result.map(pair.second) + == Ok(36) + + assert glua.run(state, { + use _ <- glua.then(glua.new_index(tbl, "other_key", glua.int(7))) + use ref <- glua.then(glua.index(tbl, "other_key")) + glua.dereference(ref, decode.int) + }) + |> result.map(pair.second) + == Ok(49) + + let assert Ok(#(state, tbl)) = + glua.run(glua.new(), { + use tbl <- glua.then(glua.table([])) + use mt <- glua.then( + glua.table([ + #( + glua.string("__newindex"), + glua.function(fn(_) { glua.error("read-only table") }), + ), + ]), + ) + + use _ <- glua.then( + glua.call_function_by_name(["setmetatable"], [tbl, mt]), + ) + glua.success(tbl) + }) + + let assert Error(glua.LuaRuntimeException( + exception: glua.ErrorCall(message: "read-only table", level: _), + state: _, + )) = glua.run(state, glua.new_index(tbl, "my_key", glua.string("my value"))) +}