Skip to content

Commit b2cab3c

Browse files
unfetchableaatxe
andauthored
Implement TOML serdes std library (#79)
Derived from [bjcscat's JSON serdes std library](#67) Closes #64 This PR adds TOML serialization and deserialization to Lute, enabling migration from tools that use TOML configuration files. Features included: - Key-value pairs (string, number, boolean) - Nested tables ([section]) - Array-of-tables ([[array]]) - Inline tables ({ key = value }) Buffer-based serialization for performance Future improvements: Datetime support. --------- Co-authored-by: ariel <aweiss@hey.com>
1 parent 254d950 commit b2cab3c

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed

batteries/toml.luau

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
local toml = {}
2+
3+
-- serialization
4+
type SerializerState = {
5+
buf: buffer,
6+
cursor: number,
7+
}
8+
9+
local function serializeValue(value: string | number)
10+
if typeof(value) == "string" then
11+
value = string.gsub(value, '\\', '\\\\')
12+
value = string.gsub(value, '\n', '\\n')
13+
value = string.gsub(value, '\t', '\\t')
14+
return "\"" .. value .. "\""
15+
elseif value == math.huge then
16+
return "inf"
17+
elseif value == -math.huge then
18+
return "-inf"
19+
elseif value ~= value then
20+
return "nan"
21+
else
22+
return tostring(value)
23+
end
24+
end
25+
26+
local function hasNestedTables(tbl: {})
27+
for _, v in tbl do
28+
if typeof(v) == "table" and next(v) ~= nil then
29+
return true
30+
end
31+
end
32+
return false
33+
end
34+
35+
local function tableToToml(tbl: {}, parent: string)
36+
local result = ""
37+
local subTables = {}
38+
local hasDirectValues = false
39+
40+
for k, v in tbl do
41+
if typeof(v) == "table" and next(v) ~= nil then
42+
if #v > 0 then
43+
for _, entry in v do
44+
result ..= "\n[[" .. (parent and parent .. "." or "") .. k .. "]]\n"
45+
result ..= tableToToml(entry, nil)
46+
end
47+
else
48+
subTables[k] = v
49+
end
50+
else
51+
hasDirectValues = true
52+
result ..= k .. " = " .. serializeValue(v) .. "\n"
53+
end
54+
end
55+
56+
for k, v in subTables do
57+
local key = parent and (parent .. "." .. k) or k
58+
59+
if hasNestedTables(v) == true then
60+
result ..= tableToToml(v, key)
61+
continue
62+
end
63+
64+
if hasDirectValues or parent then
65+
result ..= "\n[" .. key .. "]\n"
66+
end
67+
68+
result ..= tableToToml(v, key)
69+
end
70+
71+
return result
72+
end
73+
74+
75+
local function serialize(tbl: {}): string
76+
return tableToToml(tbl, nil)
77+
end
78+
79+
-- deserialization
80+
type DeserializerState = {
81+
buf: string,
82+
cursor: number,
83+
}
84+
85+
local function skipWhitespace(state: DeserializerState)
86+
local pos = state.cursor
87+
while pos <= string.len(state.buf) and string.match(string.sub(state.buf, pos, pos), "%s") do
88+
pos += 1
89+
end
90+
state.cursor = pos
91+
end
92+
93+
local function readLine(state: DeserializerState)
94+
local nextLine = string.find(state.buf, "\n", state.cursor) or string.len(state.buf) + 1
95+
local line = string.sub(state.buf, state.cursor, nextLine - 1)
96+
state.cursor = nextLine + 1
97+
return line
98+
end
99+
100+
local function deserialize(input: string)
101+
local state: DeserializerState = {
102+
buf = input,
103+
cursor = 1,
104+
}
105+
local result = {}
106+
local currentTable = result
107+
local arrayTables = {}
108+
109+
while state.cursor <= string.len(state.buf) do
110+
skipWhitespace(state)
111+
local line = readLine(state)
112+
113+
if line == "" or string.sub(line, 1, 1) == "#" then
114+
continue
115+
end
116+
117+
if string.match(line, "^%[%[(.-)%]%]$") then
118+
local tableName = string.match(line, "^%[%[(.-)%]%]$")
119+
arrayTables[tableName] = arrayTables[tableName] or {}
120+
121+
local newEntry = {}
122+
table.insert(arrayTables[tableName], newEntry)
123+
124+
result[tableName] = arrayTables[tableName]
125+
currentTable = newEntry
126+
elseif string.match(line, "^%[(.-)%]$") then
127+
local tablePath = string.match(line, "^%[(.-)%]$")
128+
local parent = result
129+
130+
for section in string.gmatch(tablePath, "([^.]+)") do
131+
if not parent[section] then parent[section] = {} end
132+
parent = parent[section]
133+
end
134+
135+
currentTable = parent
136+
elseif string.match(line, "^(.-)%s*=%s*(.-)$") then
137+
local key, value = string.match(line, "^(.-)%s*=%s*(.-)$")
138+
key = string.match(key, "^%s*(.-)%s*$")
139+
value = string.match(value, "^%s*(.-)%s*$")
140+
141+
if string.match(value, '^"(.*)"$') or string.match(value, "^'(.*)'$") then
142+
value = string.sub(value, 2, -2)
143+
value = string.gsub(value, '\\\\', '\\')
144+
value = string.gsub(value, '\\n', '\n')
145+
value = string.gsub(value, '\\t', '\t')
146+
elseif tonumber(value) then
147+
value = tonumber(value)
148+
elseif value == "true" then
149+
value = true
150+
elseif value == "false" then
151+
value = false
152+
elseif value == "inf" or value == "+inf" then
153+
value = math.huge
154+
elseif value == "-inf" then
155+
value = -math.huge
156+
elseif value == "nan" then
157+
value = 0/0
158+
end
159+
160+
currentTable[key] = value
161+
end
162+
end
163+
164+
return result
165+
end
166+
167+
-- user-facing
168+
toml.serialize = serialize
169+
toml.deserialize = deserialize
170+
171+
return toml

examples/toml.luau

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
local toml = require("@batteries/toml")
2+
3+
local deserialized = toml.deserialize([=[
4+
hello = "world"
5+
infinity = inf
6+
nan_value = nan
7+
escaped_string = "This is a string with a newline\ncharacter"
8+
9+
[world]
10+
v1 = "hi"
11+
v2 = 8080
12+
v3 = true
13+
14+
[[users]]
15+
name = "Alice"
16+
age = 25
17+
18+
[[users]]
19+
name = "Bob"
20+
age = 30
21+
22+
[people.Tom]
23+
age = 33
24+
]=])
25+
26+
local serialized = toml.serialize(deserialized)
27+
28+
print(serialized)
29+
30+
print(deserialized)

0 commit comments

Comments
 (0)