From 890f0bdee3179473e8a2f532c3f529e4a5d1e19b Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Sun, 8 Jun 2025 14:40:08 -0400 Subject: [PATCH] feat: guards for encoded Lua values Adds the following guards for use in `deflua` functions - `is_table/1` - `is_userdata/1` - `is_lua_func/1` - `is_erl_func/1` - `is_mfa/1` --- CHANGELOG.md | 8 +++++++ lib/lua/api.ex | 52 +++++++++++++++++++++++++++++++++++++++++- lib/lua/util.ex | 2 +- test/lua/api_test.exs | 53 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 091373a..d3e65fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Guards for encoded Lua values in `deflua` functions + - `is_table/1` + - `is_userdata/1` + - `is_lua_func/1` + - `is_erl_func/1` + - `is_mfa/1` + ### Fixed - `deflua` function can now specify guards when using or not using state diff --git a/lib/lua/api.ex b/lib/lua/api.ex index 160942a..d761514 100644 --- a/lib/lua/api.ex +++ b/lib/lua/api.ex @@ -74,8 +74,22 @@ defmodule Lua.API do ~LUA[print("Hello at install time!")]c end end + + ## Guards + + When doing `use Lua.API`, we also import the guards documented in this API. + This can be useful for having different function heads that match on encoded + values. E.g. + + deflua say_type(value) when is_table(value), do: "table" + deflua say_type(value) when is_userdata(value), do: "table" + + Keep in mind that if you want to work with values passed to `deflua` functions, + they still need to be decoded first. """ + require Record + defmacro __using__(opts) do scope = opts |> Keyword.get(:scope, "") |> String.split(".", trim: true) @@ -85,7 +99,17 @@ defmodule Lua.API do @before_compile Lua.API import Lua.API, - only: [runtime_exception!: 1, deflua: 2, deflua: 3, validate_func!: 3] + only: [ + runtime_exception!: 1, + deflua: 2, + deflua: 3, + validate_func!: 3, + is_table: 1, + is_userdata: 1, + is_lua_func: 1, + is_erl_func: 1, + is_mfa: 1 + ] @impl Lua.API def scope do @@ -100,6 +124,32 @@ defmodule Lua.API do @callback install(Lua.t(), scope_def(), any()) :: Lua.t() | Lua.Chunk.t() | String.t() @optional_callbacks [install: 3] + @doc """ + Is the value a reference to a Lua table? + + """ + defguard is_table(record) when Record.is_record(record, :tref) + + @doc """ + Is the value a reference to userdata? + """ + defguard is_userdata(record) when Record.is_record(record, :usdref) + + @doc """ + Is the value a reference to a Lua function? + """ + defguard is_lua_func(record) when Record.is_record(record, :funref) + + @doc """ + Is the value a reference to an Erlang / Elixir function? + """ + defguard is_erl_func(record) when Record.is_record(record, :erl_func) + + @doc """ + Is the value a reference to an Erlang / Elixir mfa? + """ + defguard is_mfa(record) when Record.is_record(record, :erl_mfa) + @doc """ Raises a runtime exception inside an API function, displaying contextual information about where the exception was raised. diff --git a/lib/lua/util.ex b/lib/lua/util.ex index 6ee798a..5b78da9 100644 --- a/lib/lua/util.ex +++ b/lib/lua/util.ex @@ -26,7 +26,7 @@ defmodule Lua.Util do # take out when https://github.com/rvirding/luerl/pull/213 # is released def encoded?(number) when is_number(number), do: true - def encoded?(table_ref) when Record.is_record(table_ref, :tref), do: true + def encoded?(record) when Record.is_record(record, :tref), do: true def encoded?(record) when Record.is_record(record, :usdref), do: true def encoded?(record) when Record.is_record(record, :funref), do: true def encoded?(record) when Record.is_record(record, :erl_func), do: true diff --git a/test/lua/api_test.exs b/test/lua/api_test.exs index 3729732..77f7866 100644 --- a/test/lua/api_test.exs +++ b/test/lua/api_test.exs @@ -336,4 +336,57 @@ defmodule Lua.APITest do assert {"not a int", _} = module.with_state(true, Lua.new()) end end + + describe "guards" do + test "can use in functions" do + assert [{module, _}] = + Code.compile_string(""" + defmodule GuardCheck do + use Lua.API, scope: "guard" + + deflua type(value) when is_table(value) do + "table" + end + + deflua type(value) when is_userdata(value) do + "userdata" + end + + deflua type(value) when is_lua_func(value) do + "lua function" + end + + deflua type(value) when is_erl_func(value) do + "erl function" + end + + deflua type(value) when is_mfa(value) do + "mfa" + end + + deflua type(_value) do + "other" + end + end + """) + + lua = + Lua.load_api(Lua.new(), module) + |> Lua.set!(["foo"], {:userdata, URI.parse("https://tvlabs.ai")}) + + assert {["table"], _} = Lua.eval!(lua, "return guard.type({})") + assert {["userdata"], _} = Lua.eval!(lua, "return guard.type(foo)") + + assert {["lua function"], _} = + Lua.eval!(lua, """ + return guard.type(function() + return 42 + end) + """) + + assert {["erl function"], _} = Lua.eval!(lua, "return guard.type(guard.type)") + assert {["mfa"], _} = Lua.eval!(lua, "return guard.type(string.lower)") + assert {["other"], _} = Lua.eval!(lua, "return guard.type(5)") + end + end end