Repository: tv-labs/lua
Pull request #72 (2025-05-13 15:46:30)

feat: private state API
Allows setting, getting, and deleting private state


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1d83a8..5d40c68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+
+## [v0.1.1] - 2025-05-13
+
+### Added
+- `Lua.put_private/3`, `Lua.get_private/2`, `Lua.get_private!/2`, and `Lua.delete_private/2` for working with private state
+
 ## [v0.1.0] - 2025-05-12
 
 ### Fixed
@@ -20,5 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Tables must now be explicitly decoded when receiving as arguments `deflua` and other Elixir callbacks
 
 
-[unreleased]: https://github.com/tv-labs/lua/compare/v0.1.0...HEAD
+[unreleased]: https://github.com/tv-labs/lua/compare/v0.1.1...HEAD
+[0.1.1]: https://github.com/tv-labs/lua/compare/v0.1.0...v0.1.1
 [0.1.0]: https://github.com/tv-labs/lua/compare/v0.0.22...v0.1.0
diff --git a/README.md b/README.md
index f409400..a3e5d07 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,38 @@ lua = Lua.new() |> Lua.load_api(Queue)
 ["first", "second"] = Lua.Table.as_list(queue)
 ```
 
+## Accessing private state from Elixir
+
+When building applications with `Lua`, you may find yourself in need of propagating extra context for use in your APIs. For instance, you may want to access information about the current user who executed the Lua script, an API key, or something else that is private and should not be available to the Lua code. For this, we have the `Lua.put_private/3`, `Lua.get_private/2`, and `Lua.delete_private/2` functions.
+
+For example, imagine you wanted to allow the user to access information about themselves
+
+``` elixir
+defmodule User do
+  defstruct [:name]
+end
+
+defmodule UserAPI do
+  use Lua.API, scope: "user"
+  
+  deflua name(), state do
+    user = Lua.get_private!(state, :user) 
+    
+    {[user.name], state}
+  end
+end
+
+user = %User{name: "Robert Virding"}
+
+lua = Lua.new() |> Lua.put_private(:user, user) |> Lua.load_api(UserAPI)
+
+{["Hello Robert Virding"], _lua} = Lua.eval!(lua, ~LUA"""
+  return "Hello " .. user.name()
+""")
+```
+
+This allows you to have simple, expressive APIs that access context that is unavailable to the Lua code.
+
 ## Credits
 
 `Lua` piggy-backs off of Robert Virding's [Luerl](https://github.com/rvirding/luerl) project, which implements a Lua lexer, parser, and full-blown Lua virtual machine that runs inside the BEAM.
diff --git a/guides/working-with-lua.livemd b/guides/working-with-lua.livemd
index 2059d1f..d7e2b1e 100644
--- a/guides/working-with-lua.livemd
+++ b/guides/working-with-lua.livemd
@@ -4,7 +4,7 @@
 
 ```elixir
 Mix.install([
-  {:lua, "~> 0.1.0"}
+  {:lua, "~> 0.1.1"}
 ])
 ```
 
diff --git a/lib/lua.ex b/lib/lua.ex
index e44b51d..91aa0e2 100644
--- a/lib/lua.ex
+++ b/lib/lua.ex
@@ -604,6 +604,54 @@ defmodule Lua do
     |> Lua.API.install(module, scope, opts[:data])
   end
 
+  @doc """
+  Puts a private value in storage for use in Elixir functions
+
+      iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
+  """
+  def put_private(%__MODULE__{} = lua, key, value) do
+    update_in(lua.state, fn state -> :luerl.put_private(key, value, state) end)
+  end
+
+  @doc """
+  Gets a private value in storage for use in Elixir functions
+
+      iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
+      iex> Lua.get_private(lua, :api_key)
+      {:ok, "1234"}
+  """
+  def get_private(%__MODULE__{state: state}, key) do
+    {:ok, :luerl.get_private(key, state)}
+  rescue
+    KeyError -> :error
+  end
+
+  @doc """
+  Gets a private value in storage for use in Elixir functions, raises if the key doesn't exist
+
+      iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
+      iex> Lua.get_private!(lua, :api_key)
+      "1234"
+  """
+  def get_private!(%__MODULE__{} = lua, key) do
+    case get_private(lua, key) do
+      {:ok, value} -> value
+      :error -> raise "private key `#{inspect(key)}` does not exist"
+    end
+  end
+
+  @doc """
+  Deletes a key from private storage
+
+      iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
+      iex> lua = Lua.delete_private(lua, :api_key)
+      iex> Lua.get_private(lua, :api_key)
+      :error
+  """
+  def delete_private(%__MODULE__{} = lua, key) do
+    update_in(lua.state, fn state -> :luerl.delete_private(key, state) end)
+  end
+
   defp wrap_variadic_function(module, function_name, with_state?) do
     # formatted = Util.format_function(module.scope() ++ [function_name], 0)
 
diff --git a/mix.exs b/mix.exs
index bd8ee9b..0f61513 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.1.0"
+  @version "0.1.1"
 
   def project do
     [


Test patch:
diff --git a/test/lua_test.exs b/test/lua_test.exs
index f4ac6d0..1a39787 100644
--- a/test/lua_test.exs
+++ b/test/lua_test.exs
@@ -1123,6 +1123,28 @@ defmodule LuaTest do
     end
   end
 
+  describe "private data" do
+    test "it can get, set, and delete private data" do
+      lua = Lua.new()
+
+      assert :error = Lua.get_private(lua, :foo)
+      assert lua = Lua.put_private(lua, :foo, 1)
+      assert {:ok, 1} = Lua.get_private(lua, :foo)
+      assert lua = Lua.put_private(lua, :foo, 2)
+      assert 2 = Lua.get_private!(lua, :foo)
+
+      assert_raise RuntimeError, "private key `:bar` does not exist", fn ->
+        Lua.get_private!(lua, :bar)
+      end
+
+      assert lua = Lua.delete_private(lua, :foo)
+      assert :error = Lua.get_private(lua, :foo)
+
+      # Delete a key that doesn't exist
+      assert Lua.delete_private(lua, :nope)
+    end
+  end
+
   defp test_file(name) do
     Path.join(["test", "fixtures", name])
   end