parent
6e5a6dcc92
commit
ec35dc911b
@ -0,0 +1,24 @@ |
||||
# `a` standard library |
||||
|
||||
Reference docs for the library `a` adds on top of Lua 5.5. Every module below is |
||||
a **global** — there is no `require`; they are present in every script. |
||||
|
||||
## Modules |
||||
|
||||
| Module | What it covers | |
||||
|--------|----------------| |
||||
| [`utils`](utils.md) | JSON encode/decode, the `NULL` marker, error-trapping (`try`/`tryn`), value `dump`. | |
||||
| [`math`](math.md) | Additions to stock `math`: `round`, `clamp`, `isFinite`, `sign`, `scale`. | |
||||
| [`table`](table.md) | Additions to stock `table`: map/filter/reduce, merge, deep-copy, aggregates, lookups, read-only proxy. | |
||||
| [`os`](os.md) | Sandbox additions: `microtime` (sub-second clock) and async `sleep`. | |
||||
| [`log`](log.md) | Level-tagged diagnostic logging (`trace`…`error`), gated by `RUST_LOG`. | |
||||
| [`sqlite`](sqlite.md) | Embedded SQLite: connect, query, parameters, transactions. | |
||||
| [`http`](http.md) | HTTP/HTTPS client: requests, JSON, auth, cookie-jar sessions. | |
||||
|
||||
## Concepts |
||||
|
||||
| Page | What it covers | |
||||
|------|----------------| |
||||
| [Concurrency](concurrency.md) | How async calls suspend, and running work in parallel with `task.join`. | |
||||
|
||||
See the top-level [README](../README.md) for what `a` is and how to build it. |
||||
@ -0,0 +1,86 @@ |
||||
# `log` |
||||
|
||||
The `log` global is for diagnostic output that is separate from a script's |
||||
results. Where `print` writes to standard output, `log` emits **level-tagged** |
||||
messages whose visibility is controlled at runtime by an environment variable — |
||||
so the same script can run quietly in production and verbosely while you debug, |
||||
with no code change. It is always available as the global `log`. |
||||
|
||||
```lua |
||||
log.info("starting run", os.time()) |
||||
log.warn("retrying after error") |
||||
log.error("giving up") |
||||
``` |
||||
|
||||
## Levels |
||||
|
||||
There is one function per severity level: |
||||
|
||||
| Function | Level | Use for | |
||||
|----------------|---------|--------------------------------------------------| |
||||
| `log.trace` | TRACE | Very fine-grained, step-by-step detail. | |
||||
| `log.debug` | DEBUG | Developer diagnostics. | |
||||
| `log.info` | INFO | Normal high-level progress. | |
||||
| `log.warn` | WARN | Something unexpected but recoverable. | |
||||
| `log.error` | ERROR | A failure worth reporting. | |
||||
|
||||
`log.warning` is an alias for `log.warn`. |
||||
|
||||
Each takes any number of arguments, converts each with `tostring`, and joins them |
||||
with a **tab**, just like `print`: |
||||
|
||||
```lua |
||||
log.info("user", userId, "logged in") --> user 42 logged in |
||||
``` |
||||
|
||||
To log a table's structure, render it first with |
||||
[`utils.dump`](utils.md#utilsdumpvalue): |
||||
|
||||
```lua |
||||
log.debug("request opts:", utils.dump(opts)) |
||||
``` |
||||
|
||||
## Controlling what is shown |
||||
|
||||
Log output goes to **standard error**, and which levels are shown is set by the |
||||
`RUST_LOG` environment variable. By default only `WARN` and `ERROR` appear: |
||||
|
||||
```sh |
||||
a script.lua # default: warnings and errors only |
||||
RUST_LOG=info a script.lua # info and above |
||||
RUST_LOG=debug a script.lua # debug and above |
||||
RUST_LOG=trace a script.lua # everything |
||||
``` |
||||
|
||||
A level shows itself and everything more severe — `RUST_LOG=info` includes |
||||
`info`, `warn`, and `error` but not `debug` or `trace`. Messages below the active |
||||
level are discarded cheaply, so leaving `log.debug` calls in place costs almost |
||||
nothing when they are not enabled. |
||||
|
||||
## `log` vs. `print` |
||||
|
||||
| | `print` | `log` | |
||||
|---|---|---| |
||||
| Stream | stdout | stderr | |
||||
| Always shown? | yes | only at/above the active level | |
||||
| Tagged with level? | no | yes | |
||||
|
||||
Use `print` for a script's actual output — the thing a caller pipes or captures. |
||||
Use `log` for the running commentary about *how* it is going, which you want to |
||||
turn up or down without editing the script. |
||||
|
||||
```lua |
||||
local rows = con:query("SELECT * FROM users") |
||||
log.info("fetched", #rows, "rows") -- diagnostic; hidden unless RUST_LOG=info |
||||
for _, r in ipairs(rows) do |
||||
print(r.name) -- the actual result |
||||
end |
||||
``` |
||||
|
||||
## Notes |
||||
|
||||
- **Always loaded.** No `require`; `log` is a global in every script. |
||||
- **Quiet by default.** With no `RUST_LOG` set, only `warn`/`error` are emitted, |
||||
so `info`/`debug`/`trace` are silent until you opt in. |
||||
- **Arguments are stringified like `print`.** Multiple arguments are joined with |
||||
tabs; tables print as their address, so use `utils.dump` for contents. |
||||
@ -0,0 +1,106 @@ |
||||
# `math` |
||||
|
||||
`a` keeps Lua's standard `math` library intact and adds a handful of functions |
||||
that come up constantly in everyday scripting: rounding, clamping, sign, finiteness, |
||||
and linear rescaling. The additions live on the same global `math` table, so |
||||
`math.floor` and `math.round` sit side by side. |
||||
|
||||
```lua |
||||
print(math.round(3.14159, 2)) --> 3.14 |
||||
print(math.clamp(120, 0, 100)) --> 100 |
||||
print(math.scale(0.5, 0, 1, 0, 255)) --> 127.5 |
||||
``` |
||||
|
||||
Everything here errors on a non-number argument rather than coercing it, so a |
||||
stray `nil` or string surfaces at the call site instead of producing a silent |
||||
`NaN`. |
||||
|
||||
## `math.round(value [, places])` |
||||
|
||||
Round to the nearest integer, or to `places` decimal places. Rounding is **half |
||||
away from zero** (so `0.5` → `1`, `-0.5` → `-1`), computed exactly — it does not |
||||
suffer the `floor(x + 0.5)` error that mis-rounds values just under `.5`. |
||||
|
||||
The return *type* depends on `places`: |
||||
|
||||
- `places` omitted or `0` → an **integer**, |
||||
- `places > 0` → a **float** rounded to that many decimals. |
||||
|
||||
```lua |
||||
math.round(2.5) --> 3 (integer) |
||||
math.round(-2.5) --> -3 |
||||
math.round(3.14159, 2) --> 3.14 (float) |
||||
math.round(7) --> 7 (integers pass through) |
||||
``` |
||||
|
||||
`places` must be a non-negative integer. A non-finite `value` (`NaN`/infinity) or |
||||
a result outside Lua's integer range is an error. |
||||
|
||||
## `math.clamp(value, min, max)` |
||||
|
||||
Constrain `value` to the interval `[min, max]`. Either bound may be `nil` to |
||||
leave that side open: |
||||
|
||||
```lua |
||||
math.clamp(15, 0, 10) --> 10 |
||||
math.clamp(-3, 0, 10) --> 0 |
||||
math.clamp(5, 0, 10) --> 5 |
||||
math.clamp(-3, 0, nil) --> 0 (no upper bound) |
||||
math.clamp(99, nil, 10) --> 10 (no lower bound) |
||||
``` |
||||
|
||||
It is an error for `min` to exceed `max`. |
||||
|
||||
## `math.isFinite(value)` |
||||
|
||||
Return `true` if `value` is a number that is neither `NaN` nor infinite. Anything |
||||
that is not a number returns `false` (it does not error), making it a safe guard |
||||
before arithmetic: |
||||
|
||||
```lua |
||||
math.isFinite(1.5) --> true |
||||
math.isFinite(1/0) --> false (infinity) |
||||
math.isFinite(0/0) --> false (NaN) |
||||
math.isFinite("x") --> false |
||||
``` |
||||
|
||||
## `math.sign(value)` |
||||
|
||||
Return the sign of a number as `-1`, `0`, or `1`. Zero (and `NaN`) yield `0`. |
||||
|
||||
```lua |
||||
math.sign(-42) --> -1 |
||||
math.sign(0) --> 0 |
||||
math.sign(3.5) --> 1 |
||||
``` |
||||
|
||||
## `math.scale(value, inMin, inMax, outMin, outMax [, clamp])` |
||||
|
||||
Linearly remap `value` from the input range `[inMin, inMax]` onto the output |
||||
range `[outMin, outMax]`. With `clamp = true`, the result is held within the |
||||
output range for inputs that fall outside the input range. |
||||
|
||||
```lua |
||||
-- map a 0–1023 ADC reading to a 0–100 percentage |
||||
math.scale(512, 0, 1023, 0, 100) --> ~50.05 |
||||
|
||||
-- ranges may descend, and you can invert |
||||
math.scale(0, 0, 100, 100, 0) --> 100 |
||||
|
||||
-- clamp keeps out-of-range inputs inside the output band |
||||
math.scale(2.0, 0, 1, 0, 255) --> 510 (unclamped) |
||||
math.scale(2.0, 0, 1, 0, 255, true) --> 255 (clamped) |
||||
``` |
||||
|
||||
The input range cannot be empty — `inMin == inMax` is an error (it would divide |
||||
by zero). |
||||
|
||||
## Notes |
||||
|
||||
- **Additions, not replacements.** All of stock Lua's `math` — `floor`, `ceil`, |
||||
`abs`, `min`, `max`, `sqrt`, `huge`, `pi`, `tointeger`, `type`, … — is still |
||||
there unchanged. |
||||
- **`round` is the only type-changing one.** It returns an integer for |
||||
`places == 0` and a float otherwise; the rest return whatever fits naturally. |
||||
- **Aggregates over tables live in [`table`](table.md).** For the min/max/mean/sum |
||||
of a collection, see `table.min`, `table.max`, `table.mean`, `table.sum`. |
||||
@ -0,0 +1,73 @@ |
||||
# `os` |
||||
|
||||
`a` runs scripts in a sandbox, so the standard `os` table is trimmed to its |
||||
safe, time-related functions — `os.time`, `os.clock`, `os.date`, |
||||
`os.difftime`, `os.getenv` — while the parts that touch the system |
||||
(`os.execute`, `os.remove`, `os.exit`, …) are removed. To that trimmed table `a` |
||||
adds two functions: a high-resolution clock and an async sleep. |
||||
|
||||
```lua |
||||
local t0 = os.microtime() |
||||
os.sleep(0.25) |
||||
print(string.format("waited %.3fs", os.microtime() - t0)) --> waited ~0.250s |
||||
``` |
||||
|
||||
## `os.microtime()` |
||||
|
||||
Return the current time as a Unix timestamp in **seconds, as a float** with |
||||
sub-second precision — unlike `os.time`, which is whole seconds only. It is the |
||||
right tool for measuring elapsed time: |
||||
|
||||
```lua |
||||
local start = os.microtime() |
||||
doSomeWork() |
||||
local elapsed = os.microtime() - start |
||||
log.info(string.format("took %.1f ms", elapsed * 1000)) |
||||
``` |
||||
|
||||
It reads the system wall clock, so it tracks real time (and can jump if the clock |
||||
is adjusted); for interval timing the difference of two readings is what you |
||||
want. |
||||
|
||||
## `os.sleep(seconds)` |
||||
|
||||
Pause for `seconds` (a float, so `os.sleep(0.1)` is 100 ms). This is an **async** |
||||
sleep: it suspends the script on the runtime instead of blocking the OS thread, |
||||
so concurrent work keeps running while it waits. |
||||
|
||||
```lua |
||||
os.sleep(1) -- one second |
||||
os.sleep(0.05) -- 50 milliseconds |
||||
``` |
||||
|
||||
`seconds` must be a non-negative finite number. |
||||
|
||||
Because the sleep suspends rather than blocks, sibling tasks started with |
||||
[`task.join`](concurrency.md) make progress during the wait — that is what lets |
||||
several sleeps (or requests, or queries) overlap: |
||||
|
||||
```lua |
||||
-- finishes in ~0.3s, not 0.6s — the sleeps overlap |
||||
task.join( |
||||
function() os.sleep(0.3) end, |
||||
function() os.sleep(0.2) end, |
||||
function() os.sleep(0.1) end |
||||
) |
||||
``` |
||||
|
||||
One caveat applies to coroutines you drive yourself with `coroutine.wrap` / |
||||
`coroutine.resume`: there, `os.sleep` does not actually wait. See |
||||
[Self-driven coroutines](concurrency.md#self-driven-coroutines) for the full |
||||
explanation — the rule is to run anything that needs to sleep through |
||||
`task.join`. |
||||
|
||||
## Notes |
||||
|
||||
- **The sandbox keeps the time functions.** `os.time`, `os.clock`, `os.date`, |
||||
`os.difftime`, and `os.getenv` work as in stock Lua; system-mutating functions |
||||
are not present. |
||||
- **`microtime` for durations, `time` for timestamps.** Use `os.microtime` when |
||||
you need sub-second precision or are timing an interval; `os.time` when whole |
||||
seconds suffice. |
||||
- **`sleep` is cooperative.** It yields to the runtime, so it is cheap to sleep |
||||
inside concurrent tasks — see [Concurrency](concurrency.md). |
||||
@ -0,0 +1,239 @@ |
||||
# `table` |
||||
|
||||
`a` keeps Lua's standard `table` library (`insert`, `remove`, `concat`, `sort`, |
||||
`pack`, `unpack`) and adds the higher-order and collection helpers stock Lua |
||||
leaves out: map/filter/reduce, merge and deep-copy, aggregates, lookups, and a |
||||
read-only proxy. The additions live on the same global `table`. |
||||
|
||||
```lua |
||||
local nums = {1, 2, 3, 4, 5} |
||||
local evens = table.ifilter(nums, function(n) return n % 2 == 0 end) |
||||
local doubled = table.map(nums, function(n) return n * 2 end) |
||||
print(table.sum(nums), table.max(nums)) --> 15 5 |
||||
``` |
||||
|
||||
## Sequences vs. general tables |
||||
|
||||
Lua tables are both arrays and maps, so most helpers come in two flavours. The |
||||
distinction runs through the whole module: |
||||
|
||||
- **Plain name** (`map`, `filter`, `reduce`) iterates with `pairs` — it visits |
||||
**every** key, preserves keys in the result, and the order is unspecified. |
||||
- **`i`-prefixed name** (`imap`-style: `ifilter`, `ireduce`, …) iterates with |
||||
`ipairs` over the sequence part `1..#t` **in order**, and produces a fresh |
||||
gapless sequence. |
||||
|
||||
Reach for the `i` variants when you have an array and want array semantics |
||||
(order preserved, no gaps); use the plain ones for maps or when keys matter. |
||||
|
||||
## Transforming |
||||
|
||||
### `table.map(tbl, fn)` |
||||
|
||||
Apply `fn(value)` to every value, **preserving keys**. Order unspecified. |
||||
|
||||
```lua |
||||
table.map({a = 1, b = 2}, function(v) return v * 10 end) --> {a = 10, b = 20} |
||||
``` |
||||
|
||||
### `table.filter(tbl, predicate)` |
||||
|
||||
Keep the entries where `predicate(value)` is truthy, **preserving keys**. On a |
||||
sequence this may leave gaps — use `ifilter` to avoid them. |
||||
|
||||
```lua |
||||
table.filter({a = 1, b = 2, c = 3}, function(v) return v > 1 end) --> {b = 2, c = 3} |
||||
``` |
||||
|
||||
### `table.ifilter(tbl, predicate)` |
||||
|
||||
Keep the sequence elements matching `predicate`, producing a new **gapless** |
||||
sequence in order. |
||||
|
||||
```lua |
||||
table.ifilter({1, 2, 3, 4}, function(v) return v % 2 == 0 end) --> {2, 4} |
||||
``` |
||||
|
||||
### `table.filterMap(tbl, fn)` |
||||
|
||||
Map and filter in one pass: apply `fn(value)`, and **drop entries where `fn` |
||||
returns `nil`**. Keys are preserved (may leave gaps on a sequence). |
||||
|
||||
```lua |
||||
-- keep and square only the even numbers |
||||
table.filterMap({a = 1, b = 2, c = 4}, function(v) |
||||
if v % 2 == 0 then return v * v end |
||||
end) --> {b = 4, c = 16} |
||||
``` |
||||
|
||||
### `table.ifilterMap(tbl, fn)` |
||||
|
||||
The sequence version of `filterMap`: map over `1..#t` in order, drop `nil` |
||||
results, and return a **gapless** sequence. |
||||
|
||||
```lua |
||||
table.ifilterMap({1, 2, 3, 4}, function(v) |
||||
if v % 2 == 0 then return v * 10 end |
||||
end) --> {20, 40} |
||||
``` |
||||
|
||||
## Reducing |
||||
|
||||
### `table.reduce(tbl, fn, init)` |
||||
|
||||
Fold every value into a single accumulator: `acc = fn(acc, value)`, starting from |
||||
`init`. Iterates with `pairs`, so order is unspecified — use it for |
||||
order-independent folds (sum, product, building a set). |
||||
|
||||
```lua |
||||
table.reduce({1, 2, 3}, function(acc, v) return acc + v end, 0) --> 6 |
||||
``` |
||||
|
||||
### `table.ireduce(tbl, fn, init)` |
||||
|
||||
Like `reduce` but folds the sequence part **in order**, for when order matters |
||||
(e.g. concatenation, left-to-right composition). |
||||
|
||||
```lua |
||||
table.ireduce({"a", "b", "c"}, function(acc, v) return acc .. v end, "") --> "abc" |
||||
``` |
||||
|
||||
## Aggregates |
||||
|
||||
These walk all values with `pairs`, so they work on sequences and maps alike. |
||||
Each returns `nil` for an empty table (except `sum`, which returns `0`), and a |
||||
`NaN` value anywhere propagates to the result. |
||||
|
||||
| Function | Result | |
||||
|--------------------|-----------------------------------------| |
||||
| `table.min(tbl)` | smallest value, or `nil` if empty | |
||||
| `table.max(tbl)` | largest value, or `nil` if empty | |
||||
| `table.mean(tbl)` | arithmetic mean, or `nil` if empty | |
||||
| `table.sum(tbl)` | sum of values, `0` if empty | |
||||
|
||||
```lua |
||||
local t = {4, 8, 15, 16, 23, 42} |
||||
print(table.min(t), table.max(t)) --> 4 42 |
||||
print(table.sum(t), table.mean(t)) --> 108 18.0 |
||||
``` |
||||
|
||||
## Querying |
||||
|
||||
### `table.keys(tbl)` / `table.values(tbl)` |
||||
|
||||
Return an array of all keys, or all values. Order is unspecified. |
||||
|
||||
```lua |
||||
table.keys({a = 1, b = 2}) --> {"a", "b"} (some order) |
||||
table.values({a = 1, b = 2}) --> {1, 2} (matching order) |
||||
``` |
||||
|
||||
### `table.contains(tbl, value)` |
||||
|
||||
Return `true` if any value equals `value` (compared with `==`). |
||||
|
||||
```lua |
||||
table.contains({"red", "green"}, "green") --> true |
||||
``` |
||||
|
||||
### `table.find(tbl, predicate)` |
||||
|
||||
Return the **first** value matching `predicate`, together with its key: |
||||
`(value, key)`, or `(nil, nil)` if none match. Because a matching value could |
||||
itself be `false`, test the returned **key** against `nil` to distinguish "found |
||||
`false`" from "not found". |
||||
|
||||
```lua |
||||
local v, k = table.find({10, 20, 30}, function(x) return x > 15 end) |
||||
print(v, k) --> 20 2 |
||||
``` |
||||
|
||||
### `table.isEmpty(tbl)` |
||||
|
||||
Return `true` if the table has no keys at all (faster and more correct than |
||||
`#tbl == 0`, which only inspects the sequence part). |
||||
|
||||
```lua |
||||
table.isEmpty({}) --> true |
||||
table.isEmpty({x = 1}) --> false |
||||
``` |
||||
|
||||
## Combining and copying |
||||
|
||||
### `table.merge(...)` |
||||
|
||||
Merge any number of tables into a **new** table (a shallow merge). Sequence parts |
||||
are **appended** in argument order; other keys are copied, with later arguments |
||||
overwriting earlier ones on a collision. Not recursive — for nested merges use |
||||
`deepMerge`. |
||||
|
||||
```lua |
||||
table.merge({1, 2}, {3, 4}) --> {1, 2, 3, 4} (sequences concatenate) |
||||
table.merge({a = 1, b = 2}, {b = 9}) --> {a = 1, b = 9} (later wins) |
||||
``` |
||||
|
||||
### `table.deepMerge(...)` |
||||
|
||||
Like `merge`, but where the same key holds a table on both sides, those tables |
||||
are merged **recursively**. The result shares no tables with the inputs — |
||||
everything is deep-copied — so mutating the result never touches the originals. |
||||
|
||||
```lua |
||||
table.deepMerge( |
||||
{ db = { host = "localhost", port = 5432 } }, |
||||
{ db = { port = 5433 } } |
||||
) --> { db = { host = "localhost", port = 5433 } } |
||||
``` |
||||
|
||||
### `table.deepCopy(value)` |
||||
|
||||
Return a deep copy of a table, recursively copying nested tables. Metatables are |
||||
**not** copied. Non-table values pass through unchanged. |
||||
|
||||
```lua |
||||
local original = { list = {1, 2, 3} } |
||||
local copy = table.deepCopy(original) |
||||
copy.list[1] = 99 |
||||
print(original.list[1]) --> 1 (unaffected) |
||||
``` |
||||
|
||||
`deepMerge` and `deepCopy` cap recursion at 100 levels and error beyond that, so |
||||
a cyclic table raises rather than looping forever. |
||||
|
||||
### `table.reverse(tbl)` |
||||
|
||||
Return a new array with the sequence elements in reverse order. |
||||
|
||||
```lua |
||||
table.reverse({1, 2, 3}) --> {3, 2, 1} |
||||
``` |
||||
|
||||
## Protecting |
||||
|
||||
### `table.readonly(tbl [, name])` |
||||
|
||||
Return a read-only proxy of `tbl`: reads pass through to the underlying table, |
||||
writes raise an error. The optional `name` is included in that error message. |
||||
Useful for exposing a constant namespace that callers must not mutate. |
||||
|
||||
```lua |
||||
local config = table.readonly({ retries = 3 }, "config") |
||||
print(config.retries) --> 3 |
||||
config.retries = 5 --> error: Attempt to update a read-only table config |
||||
``` |
||||
|
||||
**Caveat:** the proxy is an empty shell that forwards reads through a metatable, |
||||
so `pairs`, `next`, and `#` see **no** contents — it is meant for guarding API |
||||
tables you read by known key, not for iterable data. |
||||
|
||||
## Notes |
||||
|
||||
- **Additions, not replacements.** Stock `table.insert`, `remove`, `concat`, |
||||
`sort`, `pack`, `unpack` are all still present. |
||||
- **Map vs. sequence is the recurring choice.** Plain functions preserve keys via |
||||
`pairs` (unspecified order); `i`-prefixed ones produce gapless sequences via |
||||
`ipairs` (in order). Pick by whether you have an array or a map. |
||||
- **Most helpers return new tables.** `map`, `filter*`, `merge*`, `deepCopy`, |
||||
`reverse`, `keys`, `values` never mutate their input. |
||||
- **Type-checked.** Passing a non-table where a table is expected raises a clear |
||||
error naming the offending function. |
||||
@ -0,0 +1,179 @@ |
||||
# `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. |
||||
Loading…
Reference in new issue