Repository: tv-labs/lua
Pull request #45 (2024-09-08 23:21:24)

Change Lua.API.install/1 callback to Lua.API.install/3
Changes the arity of the API, passing the scope and data to the `install/3` callback, allowing the install callback to capture runtime data from APIs, databases, or elsewhere in the API scope for further usage.

Note that this is a **breaking change**, but since we have not released a stable version yet, consumers will need to be mindful.


Patch:
diff --git a/README.md b/README.md
index 4e54358..e6790b0 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,18 @@
 
 ## Exposing Elixir functions to Lua
 
-`Lua` provides the `deflua` macro for exposing Elixir functions to Lua
+The simplest way to expose an Elixir function to Lua is using the `Lua.set!/3` function
+
+``` elixir
+lua = 
+  Lua.set!(Lua.new(), [:sum], fn args ->
+    [Enum.sum(args)]
+  end)
+
+{[10], _} = Lua.eval!(lua, ~LUA"return sum(1, 2, 3, 4)"c)
+```
+
+For easily expressing APIs, `Lua` provides the `deflua` macro for exposing Elixir functions to Lua
 
 ``` elixir
 defmodule MyAPI do
@@ -46,7 +57,6 @@ lua = Lua.new() |> Lua.load_api(MyAPI)
   Lua.eval!(lua, ~LUA"""
   return double(5)
   """)
-
 ```
 
 ## Calling Lua functions from Elixir
diff --git a/guides/working-with-lua.livemd b/guides/working-with-lua.livemd
index 0daeace..c87bd0f 100644
--- a/guides/working-with-lua.livemd
+++ b/guides/working-with-lua.livemd
@@ -4,7 +4,7 @@
 
 ```elixir
 Mix.install([
-  {:lua, "~> 0.0.17"}
+  {:lua, "~> 0.0.18"}
 ])
 ```
 
diff --git a/lib/lua.ex b/lib/lua.ex
index f55f648..521052f 100644
--- a/lib/lua.ex
+++ b/lib/lua.ex
@@ -153,13 +153,18 @@ defmodule Lua do
 
       iex> Lua.set!(Lua.new(), [:a, :b, :c], [])
 
-  These table values are availble in lua scripts
+  These table values are availble in Lua scripts
 
       iex> lua = Lua.set!(Lua.new(), [:a, :b, :c], "nested!")
       iex> {result, _} = Lua.eval!(lua, "return a.b.c")
       iex> result
       ["nested!"]
 
+  `Lua.set!/3` can also be used to expose Elixir functions
+
+      iex> lua = Lua.set!(Lua.new(), [:sum], fn args -> [Enum.sum(args)] end)
+      iex> {[10], _lua} = Lua.eval!(lua, "return sum(1, 2, 3, 4)")
+
   """
   def set!(%__MODULE__{state: state}, keys, value) do
     {_keys, state} =
@@ -462,10 +467,16 @@ defmodule Lua do
   Inject functions written with the `deflua` macro into the Lua runtime.
 
   See `Lua.API` for more information on writing api modules
+
+  ### Options
+  * `:scope` - (optional) scope, overriding whatever is provided in `use Lua.API, scope: ...`
+  * `:data` - (optional) - data to be passed to the Lua.API.install/3 callback
+
   """
-  def load_api(lua, module, scope \\ nil) do
+  def load_api(lua, module, opts \\ []) do
+    opts = Keyword.validate!(opts, [:scope, :data])
     funcs = :functions |> module.__info__() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
-    scope = scope || module.scope()
+    scope = opts[:scope] || module.scope()
 
     lua = ensure_scope!(lua, scope)
 
@@ -483,7 +494,7 @@ defmodule Lua do
         end
       )
     end)
-    |> Lua.API.install(module)
+    |> Lua.API.install(module, scope, opts[:data])
   end
 
   defp wrap_variadic_function(module, function_name, with_state?) do
diff --git a/lib/lua/api.ex b/lib/lua/api.ex
index 1c8fa95..dd6c2a0 100644
--- a/lib/lua/api.ex
+++ b/lib/lua/api.ex
@@ -44,17 +44,17 @@ defmodule Lua.API do
 
   ## Installing an API
 
-  A `Lua.API` can provide an optional `install/1` callback, which
+  A `Lua.API` can provide an optional `install/3` 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
+  An `install/3` callback takes a `t:Lua.t/0` and should either return a
   Lua script to be evaluated, a `t:Lua.Chunk.t/0`, or return a new `t:Lua.t/0`
 
       defmodule WithInstall do
         use Lua.API, scope: "install"
 
         @impl Lua.API
-        def install(lua) do
+        def install(lua, _scope, _data) do
           Lua.set!(lua, [:foo], "bar")
         end
       end
@@ -70,7 +70,7 @@ defmodule Lua.API do
         import Lua
 
         @impl Lua.API
-        def install(_lua) do
+        def install(_lua, _scope, _data) do
           ~LUA[print("Hello at install time!")]c
         end
       end
@@ -94,9 +94,11 @@ defmodule Lua.API do
     end
   end
 
-  @callback scope :: list(String.t())
-  @callback install(Lua.t()) :: Lua.t() | String.t()
-  @optional_callbacks [install: 1]
+  @type scope_def :: list(String.t())
+
+  @callback scope :: scope_def()
+  @callback install(Lua.t(), scope_def(), any()) :: Lua.t() | String.t()
+  @optional_callbacks [install: 3]
 
   @doc """
   Raises a runtime exception inside an API function, displaying contextual
@@ -199,9 +201,9 @@ defmodule Lua.API do
   end
 
   @doc false
-  def install(lua, module) do
-    if function_exported?(module, :install, 1) do
-      case module.install(lua) do
+  def install(lua, module, scope, data) do
+    if function_exported?(module, :install, 3) do
+      case module.install(lua, scope, data) do
         %Lua{} = lua ->
           lua
 
diff --git a/mix.exs b/mix.exs
index 00565f6..b447a6b 100644
--- a/mix.exs
+++ b/mix.exs
@@ -2,7 +2,7 @@ defmodule Lua.MixProject do
   use Mix.Project
 
   @url "https://github.com/tv-labs/lua"
-  @version "0.0.17"
+  @version "0.0.18"
 
   def project do
     [


Test patch:
diff --git a/test/lua/api_test.exs b/test/lua/api_test.exs
index eea0a2d..3e16f59 100644
--- a/test/lua/api_test.exs
+++ b/test/lua/api_test.exs
@@ -3,7 +3,7 @@ defmodule Lua.APITest do
 
   alias Lua
 
-  describe "install/1 callback" do
+  describe "install/3 callback" do
     test "it can modify global lua state" do
       assert [{module, _}] =
                Code.compile_string("""
@@ -11,7 +11,7 @@ defmodule Lua.APITest do
                  use Lua.API
 
                  @impl Lua.API
-                 def install(lua) do
+                 def install(lua, _scope, _data) do
                    {[ret], lua} = Lua.call_function!(lua, [:foo], [])
 
                    Lua.set!(lua, [:from_install], ret)
@@ -40,7 +40,7 @@ defmodule Lua.APITest do
                  import Lua
 
                  @impl Lua.API
-                 def install(_lua) do
+                 def install(_lua, _scope, _data) do
                    ~LUA[whoa = "crazy"]
                  end
                end
@@ -63,7 +63,7 @@ defmodule Lua.APITest do
                  import Lua
 
                  @impl Lua.API
-                 def install(_lua) do
+                 def install(_lua, _scope, _data) do
                    ~LUA[whoa = "crazy"]c
                  end
                end
diff --git a/test/lua_test.exs b/test/lua_test.exs
index 86096cf..071e9f7 100644
--- a/test/lua_test.exs
+++ b/test/lua_test.exs
@@ -170,6 +170,13 @@ defmodule LuaTest do
 
       assert {[], %Lua{}} = Lua.eval!(lua, "foo(5)")
       assert {[2], %Lua{}} = Lua.eval!(lua, "return foo(1)")
+
+      lua =
+        Lua.set!(Lua.new(), [:sum], fn args ->
+          [Enum.sum(args)]
+        end)
+
+      assert {[10], _} = Lua.eval!(lua, ~LUA"return sum(1, 2, 3, 4)"c)
     end
 
     test "it can evaluate chunks" do
@@ -626,6 +633,19 @@ defmodule LuaTest do
       use Lua.API
     end
 
+    defmodule WithInstall do
+      use Lua.API
+
+      @impl Lua.API
+      def install(lua, scope, data) do
+        if data do
+          Lua.set!(lua, scope ++ [:foo], data)
+        else
+          ~LUA"foo = 42"c
+        end
+      end
+    end
+
     setup do
       {:ok, lua: Lua.new()}
     end
@@ -636,22 +656,22 @@ defmodule LuaTest do
     end
 
     test "injects a scoped Elixir module functions into the Lua runtime", %{lua: lua} do
-      lua = Lua.load_api(lua, TestModule, ["scope"])
+      lua = Lua.load_api(lua, TestModule, scope: ["scope"])
       assert {["test"], _} = Lua.eval!(lua, "return scope.foo('test')")
     end
 
     test "inject a variadic function", %{lua: lua} do
-      lua = Lua.load_api(lua, TestModule, ["scope"])
+      lua = Lua.load_api(lua, TestModule, scope: ["scope"])
       assert {["a-b-c"], _} = Lua.eval!(lua, "return scope.bar('a', 'b', 'c')")
     end
 
     test "inject a variadic function with state", %{lua: lua} do
-      lua = Lua.load_api(lua, TestModule, ["scope"])
+      lua = Lua.load_api(lua, TestModule, scope: ["scope"])
       assert {["a", "b", "c"], _} = Lua.eval!(lua, "return scope.with_state('a', 'b', 'c')")
     end
 
     test "injects Elixir functions that have multiple arities", %{lua: lua} do
-      lua = Lua.load_api(lua, TestModule, ["scope"])
+      lua = Lua.load_api(lua, TestModule, scope: ["scope"])
 
       assert {["a default"], _} = Lua.eval!(lua, "return scope.test(\"a\")")
       assert {["a b"], _} = Lua.eval!(lua, "return scope.test(\"a\", \"b\")")
@@ -670,6 +690,18 @@ defmodule LuaTest do
 
       {[22], _} = Lua.eval!(lua, "return global_var")
     end
+
+    test "it will call the install callback", %{lua: lua} do
+      lua = Lua.load_api(lua, WithInstall)
+
+      assert {[42], _lua} = Lua.eval!(lua, "return foo")
+    end
+
+    test "it can pass data to the install callback", %{lua: lua} do
+      lua = Lua.load_api(lua, WithInstall, data: "bananas")
+
+      assert {["bananas"], _lua} = Lua.eval!(lua, "return foo")
+    end
   end
 
   describe "examples" do
@@ -722,7 +754,7 @@ defmodule LuaTest do
     end
 
     setup do
-      %{lua: Lua.new() |> Lua.load_api(Examples, ["example"])}
+      %{lua: Lua.new() |> Lua.load_api(Examples, scope: ["example"])}
     end
 
     test "can work with numbers", %{lua: lua} do