Repository: tv-labs/lua
Pull request #49 (2024-09-27 14:57:09)

Cleanup the docs so that examples are copy-pastable
Thank you for the feedback @ivarvong


Patch:
diff --git a/README.md b/README.md
index e6790b0..45fa9fa 100644
--- a/README.md
+++ b/README.md
@@ -21,25 +21,33 @@
 
 `Lua` can be run using the `eval!/2` function
 
-``` elixir
-    iex> {[4], _} =
-    ...>   Lua.eval!(~LUA"""
-    ...>   return 2 + 2
-    ...>   """)
+    iex> {[4], _} = Lua.eval!("return 2 + 2")
 
-```
+## Compile-time validation
+
+Use the `~LUA` sigil to parse and validate your Lua code at compile time
+
+    #iex> {[4], _} = Lua.eval!(~LUA[return 2 +])
+    ** (Lua.CompilerException) Failed to compile Lua!
+
+Using the `c` modifier transforms your Lua code into a `t:Lua.Chunk.t/0` at compile-time,
+which will speed up execution at runtime since the Lua no longer needs to be parsed
+
+    iex> {[4], _} = Lua.eval!(~LUA[return 2 + 2]c)
 
 ## Exposing Elixir functions to Lua
 
 The simplest way to expose an Elixir function to Lua is using the `Lua.set!/3` function
 
 ``` elixir
+import Lua, only: [sigil_LUA: 2]
+
 lua = 
   Lua.set!(Lua.new(), [:sum], fn args ->
     [Enum.sum(args)]
   end)
 
-{[10], _} = Lua.eval!(lua, ~LUA"return sum(1, 2, 3, 4)"c)
+{[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
@@ -50,13 +58,12 @@ defmodule MyAPI do
       
   deflua double(v), do: 2 * v
 end
+
+import Lua, only: [sigil_LUA: 2]
     
 lua = Lua.new() |> Lua.load_api(MyAPI)
 
-{[10], _} =
-  Lua.eval!(lua, ~LUA"""
-  return double(5)
-  """)
+{[10], _} = Lua.eval!(lua, ~LUA[return double(5)])
 ```
 
 ## Calling Lua functions from Elixir
@@ -73,9 +80,11 @@ defmodule MyAPI do
   end
 end
 
+import Lua, only: [sigil_LUA: 2]
+
 lua = Lua.new() |> Lua.load_api(MyAPI)
 
-{["wow"], _} = Lua.eval!(lua, ~LUA"return example.foo(\"WOW\")")
+{["wow"], _} = Lua.eval!(lua, ~LUA[return example.foo("WOW")])
 ```
 
 ## Modify Lua state from Elixir
@@ -99,6 +108,8 @@ defmodule Queue do
   end
 end
 
+import Lua, only: [sigil_LUA: 2]
+
 lua = Lua.new() |> Lua.load_api(Queue)
 
 {[queue], _} =
diff --git a/lib/lua/table.ex b/lib/lua/table.ex
index 451ca2e..b667f9d 100644
--- a/lib/lua/table.ex
+++ b/lib/lua/table.ex
@@ -55,34 +55,57 @@ defmodule Lua.Table do
       iex> Lua.Table.as_string([{"a", 1}, {"b", 2}])
       "{a = 1, b = 2}"
 
+  Lists that "look" like Lua tables are treated as lists
+
       iex> Lua.Table.as_string([{1, "foo"}, {2, "bar"}])
       ~S[{"foo", "bar"}]
 
-  ### Options
-  * `:userdata` - A 1-arity function used to format userdata. Defaults to `fn _ -> "<userdata>"`
+  Lists are treated as lists
+
+      iex> Lua.Table.as_string(["a", "b", "c"])
+      ~S[{"a", "b", "c"}]
 
+  Regular maps are always treated as tables
+
+      iex> Lua.Table.as_string(%{1 => "foo", "bar" => "baz"})
+      ~S<{[1] = "foo", bar = "baz"}>
+
+  ### Options
+  * `:formatter` - A 2-arity function used to format values before serialization. The key and value
+  are passed as arguments. If there is no key, it will default to `nil`.
   """
   def as_string(table, opts \\ []) do
-    opts = Keyword.put_new(opts, :userdata, fn _ -> "<userdata>" end)
+    opts = Keyword.validate!(opts, formatter: &default_formatter/2)
 
-    "{" <> print_table(table, opts[:userdata]) <> "}"
+    "{" <> print_table(table, opts[:formatter]) <> "}"
   end
 
+  defp default_formatter(_key, value), do: value
+
   # List
-  defp print_table([{1, _} | _] = list, userdata_formatter) do
+  defp print_table([{1, _} | _] = list, formatter) do
     list
-    |> Enum.reduce([], fn {_, value}, acc ->
-      [acc, if(acc == [], do: "", else: ", "), format_value(value, userdata_formatter)]
+    |> Enum.reduce([], fn {key, value}, acc ->
+      [acc, if(acc == [], do: "", else: ", "), format_value(key, value, formatter)]
+    end)
+    |> IO.iodata_to_binary()
+  end
+
+  # List
+  defp print_table([value | _] = list, formatter) when not is_tuple(value) do
+    list
+    |> Enum.reduce([], fn value, acc ->
+      [acc, if(acc == [], do: "", else: ", "), format_value(nil, value, formatter)]
     end)
     |> IO.iodata_to_binary()
   end
 
   # Table
-  defp print_table(table, userdata_formatter) do
+  defp print_table(table, formatter) do
     table
     |> Enum.reduce([], fn {key, value}, acc ->
       key_str = format_key(key)
-      value_str = format_value(value, userdata_formatter)
+      value_str = format_value(key, value, formatter)
 
       entry = "#{key_str} = #{value_str}"
 
@@ -112,26 +135,16 @@ defmodule Lua.Table do
     end
   end
 
-  defp format_value(value, _) when is_number(value) do
-    to_string(value)
-  end
-
-  defp format_value(true, _), do: "true"
-  defp format_value(false, _), do: "false"
-  defp format_value(nil, _), do: "nil"
-
-  defp format_value(value, formatter) when is_list(value) do
-    str = print_table(value, formatter)
-
-    "{#{str}}"
-  end
-
-  defp format_value({:userdata, value}, formatter) do
-    format_value(formatter.(value), formatter)
-  end
-
-  defp format_value(value, _formatter) do
-    inspect(value)
+  defp format_value(key, value, formatter) do
+    case formatter.(key, value) do
+      list when is_list(list) -> "{#{print_table(list, formatter)}}"
+      {:userdata, _value} -> inspect("<userdata>")
+      true -> "true"
+      false -> "false"
+      nil -> nil
+      number when is_number(number) -> to_string(number)
+      other -> inspect(other)
+    end
   end
 
   @doc """


Test patch:
diff --git a/test/lua/table_test.exs b/test/lua/table_test.exs
index 1452ec2..13bfd41 100644
--- a/test/lua/table_test.exs
+++ b/test/lua/table_test.exs
@@ -5,11 +5,16 @@ defmodule Lua.TableTest do
 
   doctest Lua.Table
 
-  defmacro assert_table(table) do
-    quote bind_quoted: [table: table] do
+  defmacro assert_table(table, expected \\ :reflect) do
+    quote bind_quoted: [table: table, expected: Macro.escape(expected)] do
       assert output = Lua.Table.as_string(table)
       assert {[ret], _lua} = Lua.eval!("return " <> output)
-      assert ret == table
+
+      case expected do
+        :reflect -> assert ret == table
+        expected -> assert ret == expected
+      end
+
       output
     end
   end
@@ -34,12 +39,23 @@ defmodule Lua.TableTest do
       assert_table(big_table)
     end
 
+    test "it can handle lists of values" do
+      list = ["a", "b", "c", "d"]
+      assert_table(list, [{1, "a"}, {2, "b"}, {3, "c"}, {4, "d"}])
+    end
+
     test "it can handle useradata" do
       table = [{"a", 1}, {"b", {:userdata, ~D[2024-09-22]}}]
 
       assert Lua.Table.as_string(table) == ~S[{a = 1, b = "<userdata>"}]
 
-      assert Lua.Table.as_string(table, userdata: &inspect/1) == ~S<{a = 1, b = "~D[2024-09-22]"}>
+      assert Lua.Table.as_string(table,
+               formatter: fn
+                 _, {:userdata, value} -> inspect(value)
+                 _, value -> value
+               end
+             ) ==
+               ~S<{a = 1, b = "~D[2024-09-22]"}>
     end
 
     # We can't handle self-referential tables as