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