Lua runner with rich builtin stdlib
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
a/docs/utils.md

179 lines
5.9 KiB

# `utils`
The `utils` module is a grab-bag of helpers that the rest of the standard
library leans on: JSON encoding and decoding, a `null` marker for round-tripping
JSON and SQL, error-trapping wrappers, and a value pretty-printer. It is always
available as the global `utils`.
```lua
local data = utils.fromJSON('{"name":"Alice","age":30}')
print(data.name, data.age) --> Alice 30
print(utils.toJSON({1, 2, 3})) --> [1,2,3]
print(utils.dump({a = 1, b = {2, 3}})) --> {a=1, b={2, 3}}
```
## JSON
### `utils.toJSON(value [, pretty])`
Serialize a Lua value to a JSON string. Pass `pretty = true` for indented,
multi-line output; the default is compact.
```lua
utils.toJSON({ name = "test", count = 5 }) --> {"count":5,"name":"test"}
utils.toJSON({ name = "test" }, true) --> pretty-printed over several lines
```
How values are converted:
| Lua | JSON |
|-----------------------------|-------------------|
| `nil` | `null` |
| `utils.NULL` | `null` |
| `boolean` | `true` / `false` |
| integer | number |
| float | number |
| `string` | string |
| table (sequence) | array |
| table (other) | object |
A table is encoded as a **JSON array** when its keys are exactly the integers
`1..#t` with no gaps, and as a **JSON object** otherwise. Object keys must be
strings or integers (an integer key becomes its decimal string); any other key
type is an error.
These cases raise an error rather than producing invalid JSON:
- `NaN` or infinity (JSON has no representation for them),
- a value that has no JSON form (function, coroutine, userdata other than
`utils.NULL`),
- nesting deeper than 64 levels.
### `utils.fromJSON(string)`
Parse a JSON string into a Lua value. The inverse of `toJSON`:
| JSON | Lua |
|-------------------|----------------|
| `null` | `utils.NULL` |
| `true` / `false` | `boolean` |
| integer number | integer |
| fractional number | float |
| string | `string` |
| array | sequence table |
| object | table |
Invalid JSON raises an error. Note that JSON `null` decodes to
[`utils.NULL`](#utilsnull), **not** Lua `nil` — so a `null` inside an array does
not create a gap in the sequence.
```lua
local arr = utils.fromJSON('[1, null, 3]')
print(#arr) --> 3
print(utils.isNull(arr[2])) --> true
```
## `null`
JSON and SQL both have a `null`/`NULL` value distinct from "absent". Lua's `nil`
cannot fill that role: a table cannot *store* `nil` (assigning `nil` deletes the
key), so a literal `nil` can never survive inside a table to reach a JSON array
slot or a SQL parameter. `utils.NULL` is a sentinel that can.
### `utils.NULL`
A unique marker standing for an explicit null. Store it in a table where you mean
"null, not missing":
```lua
http.postJSON(url, { nickname = utils.NULL }) -- sends {"nickname":null}
con:execute("UPDATE u SET nickname = ?", { utils.NULL }) -- binds SQL NULL
```
It is the value `fromJSON` produces for JSON `null`, the value `toJSON` and the
`http`/`sqlite` modules turn back into `null`/`NULL`. Comparable by identity, so
`value == utils.NULL` works, but prefer `utils.isNull` for clarity.
### `utils.isNull(value)`
Return `true` if `value` is `utils.NULL`. Everything else, including Lua `nil`,
returns `false`.
```lua
utils.isNull(utils.NULL) --> true
utils.isNull(nil) --> false
utils.isNull(0) --> false
```
## Error handling
### `utils.try(fn [, ...])`
Call `fn` and trap any error, returning `(result, nil)` on success or
`(nil, err)` on failure — a more ergonomic `pcall` for the common
single-return-value case. Extra arguments are forwarded to `fn`.
```lua
local data, err = utils.try(function()
return utils.fromJSON(input)
end)
if not data then
log.error("bad JSON: " .. tostring(err))
return
end
-- use data
```
If `fn` errors with a `nil` value, `err` is the string `"unspecified error"`, so
a falsy first return reliably means failure.
### `utils.tryn(n, fn [, ...])`
Like `try`, but for a function that returns several values. `n` is how many
values `fn` returns on success. On success it returns those `n` values followed
by a trailing `nil` (the error slot); on failure it returns `n` `nil`s followed
by the error.
```lua
-- fn returns two values
local x, y, err = utils.tryn(2, function()
return parsePoint(s) -- returns x, y
end)
if err then
log.error("parse failed: " .. tostring(err))
else
print(x, y)
end
```
`n` must be an integer between 0 and 64. Use `try` when `fn` returns at most one
value; reach for `tryn` only when you genuinely need to capture several.
## Debugging
### `utils.dump(value)`
Render any Lua value as a readable string — handy for logging and quick
inspection. Strings are quoted, sequences print as `{ a, b, c }`, other tables as
`{ key = value, ... }`, and `utils.NULL` prints as `null`. Cycles are shown as
`<circular>` rather than looping forever, and nesting is capped at 20 levels.
```lua
print(utils.dump({ id = 1, tags = {"a", "b"}, parent = utils.NULL }))
--> { id = 1, parent = null, tags = { "a", "b" } }
```
This is for human eyes, not machine parsing — use `toJSON` when you need output
you can read back.
## Notes
- **`utils` is always loaded.** No `require`; the global is present in every
script.
- **`NULL` bridges three worlds.** The same marker represents null for `toJSON`,
`fromJSON`, the `http` JSON helpers, and SQL `NULL` binding in `sqlite`, so a
null value can round-trip through any of them.
- **`toJSON`/`fromJSON` are not symmetric for `nil`.** `nil` *encodes* to `null`,
but `null` *decodes* to `utils.NULL`. This is deliberate, so decoding never
silently drops array elements.