Repository: tv-labs/lua
Pull request #22 (2024-04-12 22:29:52)

Add install callback to Lua.API
Adds an install callback that allows users to run scripts and change the Lua environment after an API is loaded


Patch:
diff --git a/README.md b/README.md
index f1fa2f8..cf321b8 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
 
 ``` elixir
     iex> {[4], _} =
-    ...>   Lua.eval!("""
+    ...>   Lua.eval!(~LUA"""
     ...>   return 2 + 2
     ...>   """)
 
@@ -42,7 +42,7 @@ end
 lua = Lua.new() |> Lua.load_api(MyAPI)
 
 {[10], _} =
-  Lua.eval!(lua, """
+  Lua.eval!(lua, ~LUA"""
   return double(5)
   """)
 
@@ -64,7 +64,7 @@ end
 
 lua = Lua.new() |> Lua.load_api(MyAPI)
 
-{["wow"], _} = Lua.eval!(lua, "return example.foo(\"WOW\")")
+{["wow"], _} = Lua.eval!(lua, ~LUA"return example.foo(\"WOW\")")
 ```
 
 ## Modify Lua state from Elixir
diff --git a/lib/lua.ex b/lib/lua.ex
index 43d4ffe..6115baf 100644
--- a/lib/lua.ex
+++ b/lib/lua.ex
@@ -5,6 +5,8 @@ defmodule Lua do
              |> String.split("<!-- MDOC !-->")
              |> Enum.fetch!(1)
 
+  @type t :: %__MODULE__{}
+
   defstruct [:state]
 
   alias Luerl.New, as: Luerl
@@ -358,9 +360,9 @@ defmodule Lua do
   end
 
   @doc """
-  Inject functions written with the `deflua` macro into the Lua runtime
+  Inject functions written with the `deflua` macro into the Lua runtime.
 
-  Similar to `:luerl_new.load_module/3` but without the `install` callback
+  See `Lua.API` for more information on writing api modules
   """
   def load_api(lua, module, scope \\ nil) do
     funcs = :functions |> module.__info__() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
@@ -380,6 +382,7 @@ defmodule Lua do
         end
       )
     end)
+    |> Lua.API.install(module)
   end
 
   defp wrap_variadic_function(module, function_name, with_state?) do
diff --git a/lib/lua/api.ex b/lib/lua/api.ex
index c0362fc..c09404a 100644
--- a/lib/lua/api.ex
+++ b/lib/lua/api.ex
@@ -42,6 +42,37 @@ defmodule Lua.API do
         def baz(v), do: v
       end
 
+  ## Installing an API
+
+  A `Lua.API` can provide an optional `install/1` callback, which
+  can run arbitrary Lua code or change the `Lua` state in any way.
+
+  An `install/1` callback takes a `t:Lua.t/0` and should either return a
+  Lua script to be evaluated, or return a new `t:Lua.t/0`
+
+      defmodule WithInstall do
+        use Lua.API, scope: "install"
+
+        @impl Lua.API
+        def install(lua) do
+          Lua.set!(lua, [:foo], "bar")
+        end
+      end
+
+  If you don't need to write Elixir, but want to execute some Lua
+  to setup global variables, modify state, or expose some additonal
+  APIs, you can simply return a Lua script directly
+
+      defmodule WithLua do
+        use Lua.API, scope: "whoa"
+
+        import Lua
+
+        @impl Lua.API
+        def install(_lua) do
+          ~LUA[print("Hello at install time!")]
+        end
+      end
   """
 
   defmacro __using__(opts) do
@@ -63,6 +94,8 @@ defmodule Lua.API do
   end
 
   @callback scope :: list(String.t())
+  @callback install(Lua.t()) :: Lua.t() | String.t()
+  @optional_callbacks [install: 1]
 
   @doc """
   Raises a runtime exception inside an API function, displaying contextual
@@ -120,6 +153,26 @@ defmodule Lua.API do
     end
   end
 
+  @doc false
+  def install(lua, module) do
+    if function_exported?(module, :install, 1) do
+      case module.install(lua) do
+        %Lua{} = lua ->
+          lua
+
+        code when is_binary(code) ->
+          {_, lua} = Lua.eval!(lua, code)
+          lua
+
+        other ->
+          raise Lua.RuntimeException,
+                "Lua.API.install/1 must return %Lua{} or a Lua literal, got #{inspect(other)}"
+      end
+    else
+      lua
+    end
+  end
+
   defmacro __before_compile__(env) do
     attributes =
       env.module


Test patch:
diff --git a/test/lua/api_test.exs b/test/lua/api_test.exs
index b58a1dc..b54e41a 100644
--- a/test/lua/api_test.exs
+++ b/test/lua/api_test.exs
@@ -3,6 +3,58 @@ defmodule Lua.APITest do
 
   alias Lua
 
+  describe "install/1 callback" do
+    test "it can modify global lua state" do
+      assert [{module, _}] =
+               Code.compile_string("""
+               defmodule WithInstall do
+                 use Lua.API
+
+                 @impl Lua.API
+                 def install(lua) do
+                   {[ret], lua} = Lua.call_function!(lua, [:foo], [])
+
+                   Lua.set!(lua, [:from_install], ret)
+                 end
+
+                 deflua foo do
+                   "foo"
+                 end
+               end
+               """)
+
+      lua = Lua.load_api(Lua.new(), module)
+
+      assert {["foo"], _} =
+               Lua.eval!(lua, """
+               return from_install
+               """)
+    end
+
+    test "it can return lua code directly" do
+      assert [{module, _}] =
+               Code.compile_string("""
+               defmodule WithLua do
+                 use Lua.API
+
+                 import Lua
+
+                 @impl Lua.API
+                 def install(_lua) do
+                   ~LUA[whoa = "crazy"]
+                 end
+               end
+               """)
+
+      lua = Lua.load_api(Lua.new(), module)
+
+      assert {["crazy"], _} =
+               Lua.eval!(lua, """
+               return whoa
+               """)
+    end
+  end
+
   describe "deflua" do
     test "can access the current lua state" do
       assert [{module, _}] =