- Lua 99.4%
- Makefile 0.6%
| .anagild | ||
| .github | ||
| crypto | ||
| decimal | ||
| error | ||
| fs | ||
| json | ||
| lib | ||
| test | ||
| yaml | ||
| .fragjan | ||
| .gitignore | ||
| .merkwerk | ||
| .version_history | ||
| base64.lua | ||
| CHANGELOG.md | ||
| config_parser.lua | ||
| init.lua | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| sys.lua | ||
| time.lua | ||
| uuid.lua | ||
| VERSION | ||
daw-lua-common
Common Lua primitives used by Dragons@Work projects.
daw-lua-common provides small, dependency-conscious building blocks for Lua 5.1 and Lua 5.4 applications.
Status: Stable · Lua: 5.1 / 5.4 · License: ISC
❤️ Contribute and support our work
What is daw-lua-common?
daw-lua-common is the shared foundation for all Dragons@Work Lua projects: base64, INI config parsing, SHA-256/HMAC, fixed-point decimal arithmetic, a filesystem wrapper, JSON, ISO 8601 time, UUID v4, and YAML -- each a small, self-contained module, running under both Lua 5.1 and 5.4, without unnecessary external dependencies.
What problem does it solve?
Every DAW program eventually needs the same basic building blocks -- reading a config file, computing a hash, doing money arithmetic without floating-point rounding errors, or just handling files robustly. Without a shared library, that gets rebuilt again and again, with slightly different behavior and different error handling per project. daw-lua-common bundles all of that in one place, with a single error contract and the same architectural rules in every module -- so every further DAW program can rely on it.
Status
- Stable. Lua 5.1 and 5.4 are supported equally and tested against each other continuously.
- Fully migrated to the
daw_error/v1error contract (see below) -- no module boundary in this library returns raw Lua errors or plain error strings anymore. - Currently 13 test files, 500+ tests,
fragjan-clean.
Error contract
Every public function in daw-lua-common returns nil, err on
failure -- never an uncaught Lua error. err is not a string, but a
structured error object following the daw_error/v1 contract:
schema, id, source, code, message, optionally cause (a
cause chain) and data (structured additional information).
local base64 = require("daw.common.base64")
local daw_error = require("daw.common.error")
local out, err = base64.b64_decode("not valid base64")
if not out then
print(err.code) -- e.g. "invalid_length"
print(err.message) -- human-readable description
print(daw_error.is_error(err)) -- true
end
The full contract (field meaning, invariants, JSON representation) lives in the contracts repository: daw-contracts: daw_error/v1
The constructor itself lives in daw.common.error:
local daw_error = require("daw.common.error")
local err = daw_error.new("my.module", "something_failed", "human-readable message", {
cause = other_error, -- optional, must itself be a daw_error/v1
data = { path = "/tmp/x" }, -- optional, JSON-object-shaped extra data
})
daw_error.is_error(err) -- true
Installation
Clone as git submodule into lib/daw/common/ (gitignored):
git submodule add \
ssh://forgejo@smida.dragons-at-work.de:4022/DAW/daw-lua-common.git \
lib/daw/common
Or install system-wide:
make install
Installs to:
- OpenBSD:
/usr/local/share/lua/5.4/daw/common/ - Debian:
/usr/share/lua/5.4/daw/common/ - Arch:
/usr/share/lua/5.4/daw/common/
Usage
local uuid = require("daw.common.uuid")
local json = require("daw.common.json")
local fs = require("daw.common.fs")
Or load everything at once:
local common = require("daw.common")
Modules
base64.lua
Base64 and Base64url encode/decode. Pure Lua, no dependencies --
no shell-out, no external tools (including for the JWT-style
b64url_* functions). b64url_* implements RFC 4648 §5
(Base64url, no padding) built on top of b64_* (RFC 4648 §4,
standard alphabet, padded) -- one implementation, not two.
local base64 = require("daw.common.base64")
local enc, err = base64.b64url_encode("hello world") -- JWT-style, no padding
local dec, err = base64.b64url_decode(enc)
local enc, err = base64.b64_encode("hello world") -- standard RFC 4648, with padding
local dec, err = base64.b64_decode(enc)
config_parser.lua
INI config file parser. Returns a plain nested table.
local config = require("daw.common.config_parser")
local cfg, err = config.parse_file("/etc/steurjan/steurjan.conf")
-- cfg.server.host, cfg.server.port, cfg["server.smida"].url
Returns nil, daw_error/v1 on missing file, malformed lines, or keys
outside sections -- never throws. Section names and section types
follow the same convention: only [a-z0-9_-].
crypto/
SHA-256 hashing and HMAC. Shells out to system tools -- not pure Lua.
Split into crypto/hash.lua (SHA-256) and crypto/hmac.lua (HMAC) behind
a stable facade -- require("daw.common.crypto") unchanged.
local crypto = require("daw.common.crypto")
local h, err = crypto.hash_file("invoice.pdf") -- "sha256:2cf24dba..."
local h, err = crypto.hash_string("content") -- "sha256:2cf24dba..."
local h, err = crypto.hmac_sha256_hex("key", "msg")
local h, err = crypto.hmac_sha256_b64url("key", "msg")
Requires:
sha256sum(Linux, GNU coreutils) orsha256 -r(OpenBSD/FreeBSD) -- both present in the base system on all supported platformsopensslfor the HMAC functions
decimal/
Fixed-point decimal arithmetic for monetary values. Pure Lua.
No float. String in, string out. All functions return nil, err on
invalid input. Split into decimal/convert.lua, decimal/arith.lua,
decimal/compare.lua, and shared internals in decimal/util.lua
behind a stable facade -- require("daw.common.decimal") unchanged.
local dec = require("daw.common.decimal")
local a, err = dec.parse("10.00")
local b = dec.parse("1.19")
local net = dec.div(a, b, 2) -- target_scale is required, no default
dec.tostring(net) -- "8.40"
dec.parse(s) -- table | nil, err
dec.tostring(d) -- string | nil, err
dec.round(d, decimals) -- table | nil, err (decimals required: non-negative integer, e.g. 2 for cents -- varies per currency, no implicit default)
dec.add(a, b) -- table | nil, err
dec.sub(a, b) -- table | nil, err
dec.mul(a, b) -- table | nil, err
dec.div(a, b, target_scale) -- table | nil, err (target_scale required: non-negative integer, same rationale as decimals)
dec.cmp(a, b) -- -1|0|1 | nil, err
dec.eq(a, b) / dec.lt(a, b) / dec.lte(a, b) -- bool | nil, err
dec.abs(d) -- table | nil, err
dec.is_valid(s) -- bool (never errors)
error/
Constructor for daw_error/v1 objects -- the error contract every
other module in this library builds on. See "Error contract" above.
local daw_error = require("daw.common.error")
local err = daw_error.new("my.module", "something_failed", "message", { data = {...} })
daw_error.is_error(err) -- true
fs/
Filesystem wrapper around luafilesystem (lfs).
Returns nil, err if lfs is not installed -- never calls os.exit().
local fs = require("daw.common.fs")
fs.exists(path) -- bool | nil, err
fs.is_dir(path) -- bool | nil, err
fs.read_attrs(path) -- table | nil, err
fs.abspath(path) -- string | nil, err
fs.mkdir_p(path) -- true | nil, err
fs.list_dirs(path) -- list | nil, err (sorted)
fs.list_files(path, fn) -- list | nil, err (sorted)
fs.find_recursive(dir, fn) -- list | nil, err
fs.remove_dir_recursive(path) -- true | nil, err ('rm -rf', shells out via sys)
fs.read_file(path) -- string | nil, err
fs.write_file(path, data) -- true | nil, err (creates parents)
fs.copy(src, dst) -- true | nil, err (content-only, NOT a cp replacement -- see below)
fs.copy_dir(src, dst) -- true | nil, err (recursive content-only copy, same caveats as fs.copy)
fs.copy_preserve(src, dst) -- true | nil, err (cp -a semantics: preserves symlinks, permissions, timestamps)
fs.remove(path) -- true | nil, err
fs.rename(src, dst) -- true | nil, err
fs.open_append(path) -- handle | nil, err
fs.write_line(handle, line) -- true | nil, err
fs.close(handle)
fs.tmpdir() -- path | nil, err
fs.tmpfile() -- path | nil, err
fs.copy()/fs.copy_dir() read file content into memory and write a
new regular file -- symlinks are followed and flattened, and no
permissions/timestamps/ownership are preserved. Use
fs.copy_preserve() when cp-equivalent semantics are actually
required (backups, package installs). See
DAW/daw-lua-common#40.
Requires luafilesystem:
# OpenBSD: pkg_add lua54-lfs
# Debian: apt install lua-filesystem
# Arch: pacman -S lua54-filesystem
json/
JSON encoding/decoding. 3-tier strategy: cjson > dkjson > pure Lua.
local json = require("daw.common.json")
json.encode(t) -- string | nil, err (fastest available backend)
json.encode(t, true) -- pretty-printed -- backend-dependent: cjson
-- ignores this, dkjson and the pure-Lua
-- fallback both honor it
json.decode(s) -- table | nil, err (fastest available backend)
json.encode_canonical(t) -- string | nil, err (always sorted keys, for hashing)
print(json.backend) -- "cjson" | "dkjson" | "pure-lua"
Arrays are defined as dense 1..n sequences; tables mixing string and
integer keys, or with a gap, encode as JSON objects instead --
deliberate, documented in json/encode.lua.
Optional acceleration:
# dkjson: Debian: apt install lua-dkjson
# Arch: pacman -S lua54-dkjson
# cjson: luarocks install lua-cjson
sys.lua
Unified I/O wrapper with injectable handles.
All DAW modules use this -- never io.* or os.exit() directly.
local sys = require("daw.common.sys")
local out = sys.new()
out:write("hello") -- stdout + newline
out:write_raw("prompt: ") -- stdout, no newline
out:write_err("[ERROR] ...") -- stderr + newline
out:flush()
local line = out:read_line()
out:exit(1)
local result, err = out:run("echo hello") -- stdout string | nil, err
local rc, stdout = out:run_rc("make test") -- exit code + stdout, both stdout and stderr captured
run()/run_rc() expect a complete, already-quoted shell command --
they do not escape or quote parameters. Building a safe command from
untrusted input (e.g. a user-supplied path) is the caller's
responsibility.
Injectable handles for testing:
local out = sys.new({ stdout = mock, stderr = mock, stdin = mock })
time.lua
ISO 8601 parsing and formatting. No dependencies, no system timezone logic. Leap seconds are not supported (true of nearly all standard time libraries -- stated explicitly to avoid false expectations).
local time = require("daw.common.time")
local epoch, err = time.iso8601_to_epoch("2026-03-17T13:00:00+01:00")
local s, err = time.epoch_to_iso8601(epoch) -- "2026-03-17T12:00:00Z"
iso8601_to_epoch range-checks month (1-12), day (1-31), hour (0-23),
and minute/second (0-59) independently and rejects out-of-range
values. This is deliberately not full calendar validation -- e.g.
February 30 still passes, since that would require month-length and
leap-year rules this module doesn't otherwise need.
uuid.lua
UUID v4 generation and validation. Pure Lua, no dependencies.
local uuid = require("daw.common.uuid")
local id, err = uuid.generate() -- "550e8400-e29b-41d4-..." | nil, err
local short = uuid.short_code(id) -- "550E8400" | nil, err
local ok, err = uuid.is_valid(id) -- bool | nil, err
is_valid() performs a full RFC 4122 v4 check (hex digits, segment
lengths, version and variant nibble, case-insensitive) -- false
means "confirmed not a valid UUID v4" (not an error), nil, err
means the input wasn't even a string. short_code() works on any
string and does not itself validate the input.
yaml/
YAML reader/writer. 2-tier: lyaml > pure Lua. Covers nested maps, sequences, scalars. No anchors or block scalars.
local yaml = require("daw.common.yaml")
local data, err = yaml.load_file("tenant.yaml")
local data, err = yaml.load(yaml_string)
local out = yaml.dump(data) -- sorted keys, 2-space indent
print(yaml.backend) -- "lyaml" | "pure-lua"
Optional acceleration:
# OpenBSD: pkg_add lua-lyaml
# Debian: apt install lua-lyaml
# Arch: pacman -S lua54-lyaml
DAW integration
When running inside the Dragons@Work runtime, daw.common and its
submodules automatically register themselves via the optional global
hook _G.daw_register(name, module) -- part of the Biocodie
self-registration pattern (modules announce themselves rather than
being wired up centrally). Outside DAW this hook is simply absent
(if _G.daw_register then ...), so it's a no-op and can be ignored.
Development and Testing
make test
Example output (snapshot -- exact counts grow as tests are added):
=== daw-lua-common test suite (lua5.4) ===
[OK] test_base64.lua 17/17
[OK] test_config_parser.lua 30/30
[OK] test_crypto_hmac.lua 45/45
[OK] test_crypto.lua 11/11
[OK] test_decimal.lua 100/100
[OK] test_error.lua 25/25
[OK] test_fs.lua 100/100
[OK] test_init.lua 21/21
[OK] test_json.lua 17/17
[OK] test_sys.lua 40/40
[OK] test_time.lua 30/30
[OK] test_uuid.lua 14/14
[OK] test_yaml.lua 53/53
===================================
[OK] 13 files 503 tests passed
===================================
To pin a Lua version, create config/local.mk (gitignored):
DAW_LUA_VERSION = 5.1
Contributing
Bug reports, tests, feedback, and contributions are welcome.
The primary development repository is on Forgejo: daw-lua-common
The GitHub repository is a public mirror for better discoverability. Please submit issues and contributions to the primary repository.
Open Knowledge Needs People
We develop ideas, share knowledge, and build open source software and tools. Projects like this one don't grow on their own -- they live because people ask questions, experiment, share knowledge, find bugs, and help make things better.
You can contribute in different ways:
- reporting bugs
- reviewing code
- testing on other platforms
- improving documentation
- translating content
- or, if you like, supporting us financially
Every contribution helps us keep developing open knowledge and open source software.
Contribute and support our work →
License
Released under the ISC License as part of the Dragons@Work open source ecosystem.