Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,20 @@ luarocks install mods

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

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

> [!NOTE]
>
Expand All @@ -76,6 +77,7 @@ Thanks to these Lua ecosystem projects:
[`keyword`]: https://luamod.github.io/mods/modules/keyword
[`List`]: https://luamod.github.io/mods/modules/list
[`operator`]: https://luamod.github.io/mods/modules/operator
[`repr`]: https://luamod.github.io/mods/modules/repr
[`Set`]: https://luamod.github.io/mods/modules/set
[`str`]: https://luamod.github.io/mods/modules/str
[`stringcase`]: https://luamod.github.io/mods/modules/stringcase
Expand Down
27 changes: 14 additions & 13 deletions docs/src/modules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ description: Overview of available Mods modules and their purpose.

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

| Module | Description |
| ----------------------------------- | -------------------------------------------------------------- |
| [`is`](/modules/is) | Type predicates for Lua values and filesystem path kinds. |
| [`keyword`](/modules/keyword) | Lua keyword helpers for reserved-word checks. |
| [`List`](/modules/list) | Python-style list helpers for mapping, filtering, and slicing. |
| [`operator`](/modules/operator) | Operator helpers as functions. |
| [`Set`](/modules/set) | Set operations and helpers for unique values. |
| [`str`](/modules/str) | String utility helpers modeled after Python's `str`. |
| [`stringcase`](/modules/stringcase) | String case conversion helpers. |
| [`tbl`](/modules/tbl) | Utility functions for plain Lua tables. |
| [`template`](/modules/template) | Simple template rendering with value replacement. |
| [`utils`](/modules/utils) | Common utility helpers. |
| [`validate`](/modules/validate) | Validation helpers for Lua values. |
| Module | Description |
| ----------------------------------- | --------------------------------------------------------------- |
| [`is`](/modules/is) | Type predicates for Lua values and filesystem path kinds. |
| [`keyword`](/modules/keyword) | Lua keyword helpers for reserved-word checks. |
| [`List`](/modules/list) | Python-style list helpers for mapping, filtering, and slicing. |
| [`operator`](/modules/operator) | Operator helpers as functions. |
| [`repr`](/modules/repr) | Readable Lua value rendering with deterministic table ordering. |
| [`Set`](/modules/set) | Set operations and helpers for unique values. |
| [`str`](/modules/str) | String utility helpers modeled after Python's `str`. |
| [`stringcase`](/modules/stringcase) | String case conversion helpers. |
| [`tbl`](/modules/tbl) | Utility functions for plain Lua tables. |
| [`template`](/modules/template) | Simple template rendering with value replacement. |
| [`utils`](/modules/utils) | Common utility helpers. |
| [`validate`](/modules/validate) | Validation helpers for Lua values. |
49 changes: 49 additions & 0 deletions docs/src/modules/repr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
editLinkTarget: types/repr.lua
description: Fast, readable string rendering for Lua values and nested tables.
---

# `repr` <Badge type="warning" text="Unreleased" />

Render any Lua value as a readable string.

## Import

```lua
local mods = require("mods.repr")
```

## Usage

```lua
local out = repr({
user = { name = "Ada", role = "Engineer" },
count = 3,
msg = 'He said "hi"',
})
-- result:
-- {
-- count = 3,
-- msg = 'He said "hi"',
-- user = {
-- name = "Ada",
-- role = "Engineer"
-- }
-- }

out = repr({
user = {
name = "Ada",
meta = { role = "Engineer" },
},
})
-- result:
-- {
-- user = {
-- meta = {
-- role = "Engineer"
-- },
-- name = "Ada"
-- }
-- }
```
5 changes: 4 additions & 1 deletion mods.rockspec.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ description = {
license = "MIT",
summary = "Pure Lua modules",
detailed = [[
Mods provides small, focused Lua modules: List, Set, is, keyword, operator, str, stringcase, tbl, template, and validate.
Mods provides small, focused Lua modules:
List, Set, is, keyword, operator, repr, str, stringcase, tbl, template, and
validate.
]],
}

Expand All @@ -29,6 +31,7 @@ build = {
["mods.keyword"] = "src/mods/keyword.lua",
["mods.List"] = "src/mods/List.lua",
["mods.operator"] = "src/mods/operator.lua",
["mods.repr"] = "src/mods/repr.lua",
["mods.Set"] = "src/mods/Set.lua",
["mods.str"] = "src/mods/str.lua",
["mods.stringcase"] = "src/mods/stringcase.lua",
Expand Down
101 changes: 101 additions & 0 deletions spec/repr_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
local mods = require("mods")

local repr = mods.repr
local fmt = string.format

describe("mods.repr", function()
local fn = function() end
local co = coroutine.create(fn)
local keywords = mods.keyword.kwlist()

-- stylua: ignore
local tests = {
---------input--------|---------------------expected---------------------
{ nil , "nil" },
{ true , "true" },
{ false , "false" },
{ 42 , "42" },
{ 'He said "hi"' , [['He said "hi"']] },
{ { hello = "world" } , '{\n hello = "world"\n}' },
{ { "a", "b", "c" } , '{\n [1] = "a",\n [2] = "b",\n [3] = "c"\n}' },
{ {} , '{}' },
{ { { {} } } , '{\n [1] = {\n [1] = {}\n }\n}' },
{ fn , tostring(fn) },
{ co , tostring(co) },
}

for i = 1, #tests do
local input, expected = unpack(tests[i], 1, 2)
it(fmt("repr (%s)", inspect(input)), function()
local res = repr(input)
assert.are.equal(expected, res)
end)
end

for _, v in ipairs(keywords) do
it(fmt("repr(%q) brackets reserved keys", v), function()
local expected = '{\n ["' .. v .. '"] = true\n}'
assert.are_equal(expected, repr({ [v] = true }))
end)
end

it("renders complex nested tables with shared refs and cycles", function()
local root = { title = "root" }
local child = { name = "child" }
local leaf = { value = 99 }
root.child = child
root.self = root
root.shared_a = leaf
root.shared_b = leaf
root.list = { child, { back = root } }
child.parent = root
child.link = leaf
leaf.owner = child

local expected = [[
{
child = {
link = {
owner = <cycle>,
value = 99
},
name = "child",
parent = <cycle>
},
list = {
[1] = {
link = {
owner = <cycle>,
value = 99
},
name = "child",
parent = <cycle>
},
[2] = {
back = <cycle>
}
},
self = <cycle>,
shared_a = {
owner = {
link = <cycle>,
name = "child",
parent = <cycle>
},
value = 99
},
shared_b = {
owner = {
link = <cycle>,
name = "child",
parent = <cycle>
},
value = 99
},
title = "root"
}]]

local res = repr(root)
assert.are_equal(expected, res)
end)
end)
4 changes: 2 additions & 2 deletions src/mods/init.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
local mods = {}

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

Expand Down
109 changes: 109 additions & 0 deletions src/mods/repr.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
local mods = require("mods")

local type = type
local next = next
local concat = table.concat
local sort = table.sort
local rep = string.rep
local tostring = tostring
local gmatch = string.gmatch
local isidentifier = mods.keyword.isidentifier
local quote = mods.utils.quote

local INDENT = " "
local TYPE_RANK = { n = 0 }
for type_name in gmatch("number string boolean table function userdata thread", "%S+") do
TYPE_RANK.n = TYPE_RANK.n + 1
TYPE_RANK[type_name] = TYPE_RANK.n
end

-- Compare keys for deterministic mixed-type ordering.
local function key_less(a, b)
local ta = type(a)
local tb = type(b)
if ta ~= tb then
return TYPE_RANK[ta] < TYPE_RANK[tb]
end

if ta == "number" or ta == "string" then
return a < b
elseif ta == "boolean" then
return (not a) and b
end

return tostring(a) < tostring(b)
end

-- Sort entry records in place by their keys.
local function sort_entries(t)
sort(t, function(a, b)
return key_less(a.key, b.key)
end)
end

-- Format a table key using identifier or bracket notation.
local function render_key(k)
if type(k) == "string" then
if isidentifier(k) then
return k
end
return "[" .. quote(k) .. "]"
end
return "[" .. tostring(k) .. "]"
end

-- Recursively render a Lua value with cycle detection.
local function render(value, depth, seen)
local vt = type(value)
if vt == "string" then
return quote(value)
elseif vt ~= "table" then
return tostring(value)
end

if seen[value] then
return "<cycle>"
end
seen[value] = true

if next(value) == nil then
seen[value] = nil
return "{}"
end

local indent = rep(INDENT, depth - 1)
local out = {}
local pad = indent .. INDENT
local first = true
local entries = {}
for k, v in next, value do
entries[#entries + 1] = { key = k, value = v }
end
sort_entries(entries)
out[#out + 1] = "{\n"

for i = 1, #entries do
local entry = entries[i]
local k = entry.key
local v = entry.value
if first then
first = false
else
out[#out + 1] = ",\n"
end
out[#out + 1] = pad .. render_key(k) .. " = " .. render(v, depth + 1, seen)
end

out[#out + 1] = "\n"
out[#out + 1] = indent
out[#out + 1] = "}"
seen[value] = nil

return concat(out)
end

local function repr(v)
return render(v, 1, {})
end

return repr
1 change: 1 addition & 0 deletions types/mods.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
---@field keyword mods.keyword
---@field List mods.List
---@field operator mods.operator
---@field repr mods.repr
---@field Set mods.Set
---@field str mods.str
---@field stringcase mods.stringcase
Expand Down
18 changes: 18 additions & 0 deletions types/repr.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---@meta mods.repr

---Render any Lua value as a readable string.
---
---```lua
---repr({ a = 1, msg = 'He said "hi"' })
----- result:
----- {
----- a = 1,
----- msg = 'He said "hi"'
----- }
---```
---@alias mods.repr fun(v:any):string

---@type mods.repr
local repr

return repr
Loading