Skip to content

Commit da6bcec

Browse files
authored
feat(repr): add repr module (#7)
1 parent cc3cd66 commit da6bcec

9 files changed

Lines changed: 313 additions & 29 deletions

File tree

README.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,20 @@ luarocks install mods
4545

4646
<!-- Keep this section in sync with docs/modules/index.md. -->
4747

48-
| Module | Description |
49-
| -------------- | -------------------------------------------------------------- |
50-
| [`is`] | Type predicates for Lua values and filesystem path kinds. |
51-
| [`keyword`] | Lua keyword helpers for reserved-word checks. |
52-
| [`List`] | Python-style list helpers for mapping, filtering, and slicing. |
53-
| [`operator`] | Operator helpers as functions. |
54-
| [`Set`] | Set operations and helpers for unique values. |
55-
| [`str`] | String utility helpers modeled after Python's `str`. |
56-
| [`stringcase`] | String case conversion helpers. |
57-
| [`tbl`] | Utility functions for plain Lua tables. |
58-
| [`template`] | Simple template rendering with value replacement. |
59-
| [`utils`] | Common utility helpers. |
60-
| [`validate`] | Validation helpers for Lua values. |
48+
| Module | Description |
49+
| -------------- | --------------------------------------------------------------- |
50+
| [`is`] | Type predicates for Lua values and filesystem path kinds. |
51+
| [`keyword`] | Lua keyword helpers for reserved-word checks. |
52+
| [`List`] | Python-style list helpers for mapping, filtering, and slicing. |
53+
| [`operator`] | Operator helpers as functions. |
54+
| [`repr`] | Readable Lua value rendering with deterministic table ordering. |
55+
| [`Set`] | Set operations and helpers for unique values. |
56+
| [`str`] | String utility helpers modeled after Python's `str`. |
57+
| [`stringcase`] | String case conversion helpers. |
58+
| [`tbl`] | Utility functions for plain Lua tables. |
59+
| [`template`] | Simple template rendering with value replacement. |
60+
| [`utils`] | Common utility helpers. |
61+
| [`validate`] | Validation helpers for Lua values. |
6162

6263
> [!NOTE]
6364
>
@@ -76,6 +77,7 @@ Thanks to these Lua ecosystem projects:
7677
[`keyword`]: https://luamod.github.io/mods/modules/keyword
7778
[`List`]: https://luamod.github.io/mods/modules/list
7879
[`operator`]: https://luamod.github.io/mods/modules/operator
80+
[`repr`]: https://luamod.github.io/mods/modules/repr
7981
[`Set`]: https://luamod.github.io/mods/modules/set
8082
[`str`]: https://luamod.github.io/mods/modules/str
8183
[`stringcase`]: https://luamod.github.io/mods/modules/stringcase

docs/src/modules/index.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ description: Overview of available Mods modules and their purpose.
55

66
<!-- Keep this section in sync with README.md#modules. -->
77

8-
| Module | Description |
9-
| ----------------------------------- | -------------------------------------------------------------- |
10-
| [`is`](/modules/is) | Type predicates for Lua values and filesystem path kinds. |
11-
| [`keyword`](/modules/keyword) | Lua keyword helpers for reserved-word checks. |
12-
| [`List`](/modules/list) | Python-style list helpers for mapping, filtering, and slicing. |
13-
| [`operator`](/modules/operator) | Operator helpers as functions. |
14-
| [`Set`](/modules/set) | Set operations and helpers for unique values. |
15-
| [`str`](/modules/str) | String utility helpers modeled after Python's `str`. |
16-
| [`stringcase`](/modules/stringcase) | String case conversion helpers. |
17-
| [`tbl`](/modules/tbl) | Utility functions for plain Lua tables. |
18-
| [`template`](/modules/template) | Simple template rendering with value replacement. |
19-
| [`utils`](/modules/utils) | Common utility helpers. |
20-
| [`validate`](/modules/validate) | Validation helpers for Lua values. |
8+
| Module | Description |
9+
| ----------------------------------- | --------------------------------------------------------------- |
10+
| [`is`](/modules/is) | Type predicates for Lua values and filesystem path kinds. |
11+
| [`keyword`](/modules/keyword) | Lua keyword helpers for reserved-word checks. |
12+
| [`List`](/modules/list) | Python-style list helpers for mapping, filtering, and slicing. |
13+
| [`operator`](/modules/operator) | Operator helpers as functions. |
14+
| [`repr`](/modules/repr) | Readable Lua value rendering with deterministic table ordering. |
15+
| [`Set`](/modules/set) | Set operations and helpers for unique values. |
16+
| [`str`](/modules/str) | String utility helpers modeled after Python's `str`. |
17+
| [`stringcase`](/modules/stringcase) | String case conversion helpers. |
18+
| [`tbl`](/modules/tbl) | Utility functions for plain Lua tables. |
19+
| [`template`](/modules/template) | Simple template rendering with value replacement. |
20+
| [`utils`](/modules/utils) | Common utility helpers. |
21+
| [`validate`](/modules/validate) | Validation helpers for Lua values. |

docs/src/modules/repr.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
editLinkTarget: types/repr.lua
3+
description: Fast, readable string rendering for Lua values and nested tables.
4+
---
5+
6+
# `repr` <Badge type="warning" text="Unreleased" />
7+
8+
Render any Lua value as a readable string.
9+
10+
## Import
11+
12+
```lua
13+
local mods = require("mods.repr")
14+
```
15+
16+
## Usage
17+
18+
```lua
19+
local out = repr({
20+
user = { name = "Ada", role = "Engineer" },
21+
count = 3,
22+
msg = 'He said "hi"',
23+
})
24+
-- result:
25+
-- {
26+
-- count = 3,
27+
-- msg = 'He said "hi"',
28+
-- user = {
29+
-- name = "Ada",
30+
-- role = "Engineer"
31+
-- }
32+
-- }
33+
34+
out = repr({
35+
user = {
36+
name = "Ada",
37+
meta = { role = "Engineer" },
38+
},
39+
})
40+
-- result:
41+
-- {
42+
-- user = {
43+
-- meta = {
44+
-- role = "Engineer"
45+
-- },
46+
-- name = "Ada"
47+
-- }
48+
-- }
49+
```

mods.rockspec.template

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ description = {
1313
license = "MIT",
1414
summary = "Pure Lua modules",
1515
detailed = [[
16-
Mods provides small, focused Lua modules: List, Set, is, keyword, operator, str, stringcase, tbl, template, and validate.
16+
Mods provides small, focused Lua modules:
17+
List, Set, is, keyword, operator, repr, str, stringcase, tbl, template, and
18+
validate.
1719
]],
1820
}
1921

@@ -29,6 +31,7 @@ build = {
2931
["mods.keyword"] = "src/mods/keyword.lua",
3032
["mods.List"] = "src/mods/List.lua",
3133
["mods.operator"] = "src/mods/operator.lua",
34+
["mods.repr"] = "src/mods/repr.lua",
3235
["mods.Set"] = "src/mods/Set.lua",
3336
["mods.str"] = "src/mods/str.lua",
3437
["mods.stringcase"] = "src/mods/stringcase.lua",

spec/repr_spec.lua

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
local mods = require("mods")
2+
3+
local repr = mods.repr
4+
local fmt = string.format
5+
6+
describe("mods.repr", function()
7+
local fn = function() end
8+
local co = coroutine.create(fn)
9+
local keywords = mods.keyword.kwlist()
10+
11+
-- stylua: ignore
12+
local tests = {
13+
---------input--------|---------------------expected---------------------
14+
{ nil , "nil" },
15+
{ true , "true" },
16+
{ false , "false" },
17+
{ 42 , "42" },
18+
{ 'He said "hi"' , [['He said "hi"']] },
19+
{ { hello = "world" } , '{\n hello = "world"\n}' },
20+
{ { "a", "b", "c" } , '{\n [1] = "a",\n [2] = "b",\n [3] = "c"\n}' },
21+
{ {} , '{}' },
22+
{ { { {} } } , '{\n [1] = {\n [1] = {}\n }\n}' },
23+
{ fn , tostring(fn) },
24+
{ co , tostring(co) },
25+
}
26+
27+
for i = 1, #tests do
28+
local input, expected = unpack(tests[i], 1, 2)
29+
it(fmt("repr (%s)", inspect(input)), function()
30+
local res = repr(input)
31+
assert.are.equal(expected, res)
32+
end)
33+
end
34+
35+
for _, v in ipairs(keywords) do
36+
it(fmt("repr(%q) brackets reserved keys", v), function()
37+
local expected = '{\n ["' .. v .. '"] = true\n}'
38+
assert.are_equal(expected, repr({ [v] = true }))
39+
end)
40+
end
41+
42+
it("renders complex nested tables with shared refs and cycles", function()
43+
local root = { title = "root" }
44+
local child = { name = "child" }
45+
local leaf = { value = 99 }
46+
root.child = child
47+
root.self = root
48+
root.shared_a = leaf
49+
root.shared_b = leaf
50+
root.list = { child, { back = root } }
51+
child.parent = root
52+
child.link = leaf
53+
leaf.owner = child
54+
55+
local expected = [[
56+
{
57+
child = {
58+
link = {
59+
owner = <cycle>,
60+
value = 99
61+
},
62+
name = "child",
63+
parent = <cycle>
64+
},
65+
list = {
66+
[1] = {
67+
link = {
68+
owner = <cycle>,
69+
value = 99
70+
},
71+
name = "child",
72+
parent = <cycle>
73+
},
74+
[2] = {
75+
back = <cycle>
76+
}
77+
},
78+
self = <cycle>,
79+
shared_a = {
80+
owner = {
81+
link = <cycle>,
82+
name = "child",
83+
parent = <cycle>
84+
},
85+
value = 99
86+
},
87+
shared_b = {
88+
owner = {
89+
link = <cycle>,
90+
name = "child",
91+
parent = <cycle>
92+
},
93+
value = 99
94+
},
95+
title = "root"
96+
}]]
97+
98+
local res = repr(root)
99+
assert.are_equal(expected, res)
100+
end)
101+
end)

src/mods/init.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
local mods = {}
22

3-
([[is keyword List operator Set str stringcase
4-
tbl template utils validate]]):gsub("%S+", function(name)
3+
([[ is keyword List operator repr Set
4+
str stringcase tbl template utils validate ]]):gsub("%S+", function(name)
55
mods[name] = "mods." .. name
66
end)
77

src/mods/repr.lua

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
local mods = require("mods")
2+
3+
local type = type
4+
local next = next
5+
local concat = table.concat
6+
local sort = table.sort
7+
local rep = string.rep
8+
local tostring = tostring
9+
local gmatch = string.gmatch
10+
local isidentifier = mods.keyword.isidentifier
11+
local quote = mods.utils.quote
12+
13+
local INDENT = " "
14+
local TYPE_RANK = { n = 0 }
15+
for type_name in gmatch("number string boolean table function userdata thread", "%S+") do
16+
TYPE_RANK.n = TYPE_RANK.n + 1
17+
TYPE_RANK[type_name] = TYPE_RANK.n
18+
end
19+
20+
-- Compare keys for deterministic mixed-type ordering.
21+
local function key_less(a, b)
22+
local ta = type(a)
23+
local tb = type(b)
24+
if ta ~= tb then
25+
return TYPE_RANK[ta] < TYPE_RANK[tb]
26+
end
27+
28+
if ta == "number" or ta == "string" then
29+
return a < b
30+
elseif ta == "boolean" then
31+
return (not a) and b
32+
end
33+
34+
return tostring(a) < tostring(b)
35+
end
36+
37+
-- Sort entry records in place by their keys.
38+
local function sort_entries(t)
39+
sort(t, function(a, b)
40+
return key_less(a.key, b.key)
41+
end)
42+
end
43+
44+
-- Format a table key using identifier or bracket notation.
45+
local function render_key(k)
46+
if type(k) == "string" then
47+
if isidentifier(k) then
48+
return k
49+
end
50+
return "[" .. quote(k) .. "]"
51+
end
52+
return "[" .. tostring(k) .. "]"
53+
end
54+
55+
-- Recursively render a Lua value with cycle detection.
56+
local function render(value, depth, seen)
57+
local vt = type(value)
58+
if vt == "string" then
59+
return quote(value)
60+
elseif vt ~= "table" then
61+
return tostring(value)
62+
end
63+
64+
if seen[value] then
65+
return "<cycle>"
66+
end
67+
seen[value] = true
68+
69+
if next(value) == nil then
70+
seen[value] = nil
71+
return "{}"
72+
end
73+
74+
local indent = rep(INDENT, depth - 1)
75+
local out = {}
76+
local pad = indent .. INDENT
77+
local first = true
78+
local entries = {}
79+
for k, v in next, value do
80+
entries[#entries + 1] = { key = k, value = v }
81+
end
82+
sort_entries(entries)
83+
out[#out + 1] = "{\n"
84+
85+
for i = 1, #entries do
86+
local entry = entries[i]
87+
local k = entry.key
88+
local v = entry.value
89+
if first then
90+
first = false
91+
else
92+
out[#out + 1] = ",\n"
93+
end
94+
out[#out + 1] = pad .. render_key(k) .. " = " .. render(v, depth + 1, seen)
95+
end
96+
97+
out[#out + 1] = "\n"
98+
out[#out + 1] = indent
99+
out[#out + 1] = "}"
100+
seen[value] = nil
101+
102+
return concat(out)
103+
end
104+
105+
local function repr(v)
106+
return render(v, 1, {})
107+
end
108+
109+
return repr

types/mods.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
---@field keyword mods.keyword
66
---@field List mods.List
77
---@field operator mods.operator
8+
---@field repr mods.repr
89
---@field Set mods.Set
910
---@field str mods.str
1011
---@field stringcase mods.stringcase

types/repr.lua

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---@meta mods.repr
2+
3+
---Render any Lua value as a readable string.
4+
---
5+
---```lua
6+
---repr({ a = 1, msg = 'He said "hi"' })
7+
----- result:
8+
----- {
9+
----- a = 1,
10+
----- msg = 'He said "hi"'
11+
----- }
12+
---```
13+
---@alias mods.repr fun(v:any):string
14+
15+
---@type mods.repr
16+
local repr
17+
18+
return repr

0 commit comments

Comments
 (0)