Current section
Files
Jump to
Current section
Files
lib/lua/table.ex
defmodule Lua.Table do
@moduledoc """
In Lua, [tables](https://www.lua.org/pil/2.5.html) are the fundamental datastructure,
which are used both as associative arrays (maps), and arrays (lists).
`Lua.Table` provides some utilities for working with Lua tables when passed back to
Elixir.
"""
@doc """
Converts a Lua table into a list. Assumes that the
table is correctly ordered.
iex> Lua.Table.as_list([{1, "a"}, {2, "b"}, {3, "c"}])
["a", "b", "c"]
To ensure the list is ordered, you can pass the `:sort` option
iex> Lua.Table.as_list([{2, "b"}, {1, "a"}, {3, "c"}])
["b", "a", "c"]
iex> Lua.Table.as_list([{2, "b"}, {1, "a"}, {3, "c"}], sort: true)
["a", "b", "c"]
"""
def as_list(values, opts \\ []) do
opts = Keyword.validate!(opts, sort: false)
sorter =
if Keyword.fetch!(opts, :sort) do
&List.keysort(&1, 0)
else
&Function.identity/1
end
values
|> sorter.()
|> Enum.map(fn {_, v} -> v end)
end
@doc """
Converts a Lua table into a map
iex> Lua.Table.as_map([{"a", 1}, {"b", 2}])
%{"a" => 1, "b" => 2}
"""
def as_map(values) do
Map.new(values)
end
@doc """
Converts a Lua table into a string representing
a Lua table literal
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"}]
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.validate!(opts, formatter: &default_formatter/2)
"{" <> print_table(table, opts[:formatter]) <> "}"
end
defp default_formatter(_key, value), do: value
# List
defp print_table([{1, _} | _] = list, formatter) do
list
|> 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, formatter) do
table
|> Enum.reduce([], fn {key, value}, acc ->
key_str = format_key(key)
value_str = format_value(key, value, formatter)
entry = "#{key_str} = #{value_str}"
[acc, if(acc == [], do: "", else: ", "), entry]
end)
|> then(fn parts ->
IO.iodata_to_binary(parts)
end)
end
defp valid_identifier?(identifier) do
regex = ~r/^[[:alpha:]_][[:alnum:]_]*$/u
Regex.match?(regex, identifier)
end
defp format_key(number) when is_number(number) do
["[", to_string(number), "]"]
end
defp format_key(key) do
key = to_string(key)
if valid_identifier?(key) do
key
else
["[\"", key, "\"]"]
end
end
defp format_value(key, value, formatter) do
case formatter.(key, value) do
list when is_list(list) -> "{#{print_table(list, formatter)}}"
map when is_map(map) -> "{#{print_table(map, 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 """
Converts a Lua table into more "native" feeling lists and
maps, deeply traversing any sub-tables.
It uses the heuristic that maps with integer keys starting
as 1 will be auto-cast into lists
iex> Lua.Table.deep_cast([{"a", 1}, {"b", [{1, 3}, {2, 4}]}])
%{"a" => 1, "b" => [3, 4]}
"""
def deep_cast(value) do
case value do
[{1, _val} | _rest] = list ->
Enum.map(list, fn
{_, v} when is_list(v) -> deep_cast(v)
{_, v} -> v
end)
map ->
Map.new(map, fn
{k, v} when is_list(v) -> {k, deep_cast(v)}
{k, v} -> {k, v}
end)
end
end
end