Shared Lua utilities for Dragons@Work services (UUID, JSON, string helpers)
  • Lua 99.4%
  • Makefile 0.6%
Find a file
2026-08-06 22:25:10 +02:00
.anagild refactor!(common): finish team-review hardening pass + unify error-message contract 2026-07-20 07:12:48 +02:00
.github feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
crypto feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
decimal feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
error feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
fs feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
json feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
lib feat!: migrate to new DAW project structure (v1.0.0) 2026-05-13 18:23:34 +02:00
test feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
yaml feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
.fragjan feat(fs): add abspath to resolve relative paths (DAW/daw-lua-common#31) 2026-05-31 06:35:46 +02:00
.gitignore refactor!(common): unify error handling and remove hidden dependencies across daw-lua-common 2026-07-19 18:26:46 +02:00
.merkwerk feat(fs): add abspath to resolve relative paths (DAW/daw-lua-common#31) 2026-05-31 06:35:46 +02:00
.version_history chore: merkwerk auto-update 2026-08-06 22:25:10 +02:00
base64.lua feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
CHANGELOG.md feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
config_parser.lua feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
init.lua feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
LICENSE refactor!(common): unify error handling and remove hidden dependencies across daw-lua-common 2026-07-19 18:26:46 +02:00
Makefile feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
README.md feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
sys.lua feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
time.lua feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
uuid.lua feat!: migrate to daw_error/v1 structured error contract 2026-08-06 22:24:19 +02:00
VERSION chore: bump version to 2.1.0 2026-08-06 22:25:09 +02:00

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/v1 error 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) or sha256 -r (OpenBSD/FreeBSD) -- both present in the base system on all supported platforms
  • openssl for 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.