Merge feature/schema-first-docs (Workstream F)
This commit is contained in:
commit
3510c3ba90
7 changed files with 1152 additions and 0 deletions
|
|
@ -67,6 +67,24 @@ cargo build --release --workspace
|
||||||
cargo test --release --workspace
|
cargo test --release --workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Keeping the API docs honest
|
||||||
|
|
||||||
|
`Documentation.md`'s Lua API and IPC protocol sections are hand-written and
|
||||||
|
have drifted from the actual code before — there's a checked-in registry,
|
||||||
|
`api-schema.toml`, plus an `xtask` checker that catches it happening again.
|
||||||
|
|
||||||
|
Whenever you add, rename, or remove a `bread.*` Lua binding
|
||||||
|
(`breadd/src/lua/mod.rs`) or an IPC method (`breadd/src/ipc/mod.rs`):
|
||||||
|
|
||||||
|
1. Add/update/remove its entry in `api-schema.toml` to match.
|
||||||
|
2. Add/update the corresponding section in `Documentation.md` (a
|
||||||
|
`#### bread.<name>` heading for a Lua binding, or a row in the IPC
|
||||||
|
Methods table for an IPC method).
|
||||||
|
3. Run `cargo run -p xtask -- check-docs` before committing. It fails with
|
||||||
|
a non-zero exit and a list of exactly what's out of sync — added but
|
||||||
|
undocumented, stale in the schema, or missing a doc heading/row — if
|
||||||
|
`api-schema.toml`, the code, and `Documentation.md` don't all agree.
|
||||||
|
|
||||||
## CI
|
## CI
|
||||||
|
|
||||||
- `dev-release.yml` — triggered on push to `main`.
|
- `dev-release.yml` — triggered on push to `main`.
|
||||||
|
|
|
||||||
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -2426,6 +2426,15 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zbus"
|
name = "zbus"
|
||||||
version = "3.15.2"
|
version = "3.15.2"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ members = [
|
||||||
"breadd",
|
"breadd",
|
||||||
"bread-cli",
|
"bread-cli",
|
||||||
"bread-emit",
|
"bread-emit",
|
||||||
|
"xtask",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -544,6 +544,10 @@ bread.hyprland.dispatch("exec", "kitty")
|
||||||
-- Set a keyword
|
-- Set a keyword
|
||||||
bread.hyprland.keyword("monitor", "HDMI-A-1, 2560x1440, 0x0, 1")
|
bread.hyprland.keyword("monitor", "HDMI-A-1, 2560x1440, 0x0, 1")
|
||||||
|
|
||||||
|
-- Send a raw request to the Hyprland socket, e.g. to evaluate a config-file
|
||||||
|
-- expression the way `hyprctl eval <expr>` does; returns the raw response string
|
||||||
|
local result = bread.hyprland.eval("some expression")
|
||||||
|
|
||||||
-- Query compositor state (returns deserialized Lua tables)
|
-- Query compositor state (returns deserialized Lua tables)
|
||||||
local win = bread.hyprland.active_window()
|
local win = bread.hyprland.active_window()
|
||||||
local monitors = bread.hyprland.monitors()
|
local monitors = bread.hyprland.monitors()
|
||||||
|
|
|
||||||
500
api-schema.toml
Normal file
500
api-schema.toml
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
# Bread Automation API schema — checked-in source-of-truth registry.
|
||||||
|
#
|
||||||
|
# This is Workstream F (scoped down) from the governance-hardening report:
|
||||||
|
# a *drift detector*, not a doc generator. `Documentation.md`'s "Dictionary:
|
||||||
|
# Lua API" section is hand-written prose (one `#### bread.<name>(...)`
|
||||||
|
# heading per binding, with worked examples and edge-case notes) — nothing
|
||||||
|
# here regenerates or reformats that prose. Instead, this file is the
|
||||||
|
# checked-in list of every `bread.*` Lua binding and IPC method that is
|
||||||
|
# supposed to exist right now, and `cargo run -p xtask -- check-docs`
|
||||||
|
# cross-checks it against:
|
||||||
|
#
|
||||||
|
# 1. The actual bindings registered in breadd/src/lua/mod.rs
|
||||||
|
# (`bread.set("name", ...)` calls, the nested `<x>_tbl.set(...)` calls
|
||||||
|
# for state/profile/hyprland/widget/machine/fs/json/bluetooth, and the
|
||||||
|
# handful of bindings defined via plain embedded Lua source rather than
|
||||||
|
# `bread.set` — log/warn/error/debounce/spawn/wait/wait_any/wait_all/
|
||||||
|
# workflow.*).
|
||||||
|
# 2. The actual IPC methods dispatched in breadd/src/ipc/mod.rs's
|
||||||
|
# `match req.method.as_str() { ... }` block, plus the specially-cased
|
||||||
|
# `events.subscribe` streaming upgrade.
|
||||||
|
# 3. Documentation.md itself, to make sure each entry here still has a
|
||||||
|
# `#### bread.<name>` heading (Lua) or a row in the IPC Methods table
|
||||||
|
# (IPC methods).
|
||||||
|
#
|
||||||
|
# Whenever you add, rename, or remove a `bread.*` binding or IPC method:
|
||||||
|
# 1. Update this file to match.
|
||||||
|
# 2. Update/add the corresponding section in Documentation.md.
|
||||||
|
# 3. Run `cargo run -p xtask -- check-docs` before committing — it fails
|
||||||
|
# loudly (non-zero exit) if the three are out of sync.
|
||||||
|
#
|
||||||
|
# `kind` is one of: "lua_function", "lua_table", "ipc_method".
|
||||||
|
# `since` is the API version (Documentation.md's "API Stability &
|
||||||
|
# Versioning" section) the binding/method was introduced in. Anything from
|
||||||
|
# the original v1.0 baseline (no `*(Since: vX.Y)*` marker in Documentation.md)
|
||||||
|
# is listed as "1.0" here.
|
||||||
|
#
|
||||||
|
# Format chosen: a single checked-in TOML file (this is the "a schema file
|
||||||
|
# that's checked and diffed against the actual API surface, and CI fails the
|
||||||
|
# build if they drift" option the source report names, as opposed to Rust
|
||||||
|
# attribute macros — overkill for a ~30-binding surface with no existing
|
||||||
|
# proc-macro infrastructure in this workspace). TOML specifically because
|
||||||
|
# `toml = "0.8"` is already a dependency of breadd/bread-cli/bread-shared
|
||||||
|
# (see breadd/src/core/config.rs, bread-cli/src/modules_mgmt.rs) — no new
|
||||||
|
# format/parser needed anywhere in the ecosystem.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Events (breadd/src/lua/mod.rs install_api + install_wait_helper)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "on"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "once"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "filter"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "off"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "emit"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "wait"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "spawn"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "wait_any"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "wait_all"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Workflows (install_workflow_helpers) *(Since: v1.2)*
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflow"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflow.define"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflow.start"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflow.step"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflow.status"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflow.list"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Widgets (widget_tbl) *(Since: v1.3)*
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "widget"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.3"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "widget.register"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.3"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "widget.update"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.3"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "widget.remove"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.3"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "widget.list"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.3"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — State (state_tbl)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.get"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.monitors"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.active_workspace"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.active_window"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.devices"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.power"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.network"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.profile"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.watch"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Profiles (profile_tbl)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "profile"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "profile.activate"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Execution, notifications, timers
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "exec"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "exec_capture"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "notify"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "after"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "every"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "cancel"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Hyprland (hyprland_tbl)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.dispatch"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.keyword"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.eval"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.active_window"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.monitors"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.workspaces"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.clients"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "hyprland.on_raw"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Module declaration
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "module"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Machine and filesystem (machine_tbl / fs_tbl / json_tbl)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "machine"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "machine.name"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "machine.tags"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "machine.has_tag"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "fs"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "fs.write"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "fs.read"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "fs.exists"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "fs.readlink"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "fs.expand"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "json"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "json.decode"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Bluetooth (bluetooth_tbl)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth"
|
||||||
|
kind = "lua_table"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth.power"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth.powered"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth.connect"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth.disconnect"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth.scan"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "bluetooth.devices"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lua API — Utilities (install_log_helpers / install_debounce)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "log"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "warn"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "error"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "debounce"
|
||||||
|
kind = "lua_function"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# IPC methods (breadd/src/ipc/mod.rs handle_request + events.subscribe)
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "ping"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "health"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.get"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "state.dump"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "modules.list"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "modules.reload"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "profile.list"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "profile.activate"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "emit"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "events.subscribe"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "events.replay"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.0"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "workflows.list"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.2"
|
||||||
|
|
||||||
|
[[entry]]
|
||||||
|
name = "widgets.list"
|
||||||
|
kind = "ipc_method"
|
||||||
|
since = "1.3"
|
||||||
14
xtask/Cargo.toml
Normal file
14
xtask/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "xtask"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
toml = "0.8"
|
||||||
606
xtask/src/main.rs
Normal file
606
xtask/src/main.rs
Normal file
|
|
@ -0,0 +1,606 @@
|
||||||
|
//! Repo maintenance tasks that don't belong in the `breadd`/`bread-cli`
|
||||||
|
//! binaries themselves.
|
||||||
|
//!
|
||||||
|
//! Currently just `check-docs`: a drift detector between the actual
|
||||||
|
//! `bread.*` Lua binding surface + IPC method surface (as implemented in
|
||||||
|
//! `breadd/src/lua/mod.rs` / `breadd/src/ipc/mod.rs`) and the checked-in
|
||||||
|
//! registry at `api-schema.toml`, cross-referenced against `Documentation.md`.
|
||||||
|
//! See `api-schema.toml`'s header comment for why this exists and why it's a
|
||||||
|
//! *drift detector*, not a doc generator.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
match args.first().map(String::as_str) {
|
||||||
|
Some("check-docs") => match run_check_docs() {
|
||||||
|
Ok(true) => ExitCode::SUCCESS,
|
||||||
|
Ok(false) => ExitCode::FAILURE,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("xtask check-docs: error: {err:#}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(other) => {
|
||||||
|
eprintln!("unknown xtask command '{other}'\n{}", usage());
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
eprintln!("{}", usage());
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage() -> &'static str {
|
||||||
|
"usage: cargo run -p xtask -- <command>\n\n\
|
||||||
|
Available commands:\n \
|
||||||
|
check-docs verify api-schema.toml matches breadd's Lua/IPC surface and Documentation.md"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_check_docs() -> Result<bool> {
|
||||||
|
let root = repo_root()?;
|
||||||
|
|
||||||
|
let lua_src = std::fs::read_to_string(root.join("breadd/src/lua/mod.rs"))
|
||||||
|
.context("reading breadd/src/lua/mod.rs")?;
|
||||||
|
let ipc_src = std::fs::read_to_string(root.join("breadd/src/ipc/mod.rs"))
|
||||||
|
.context("reading breadd/src/ipc/mod.rs")?;
|
||||||
|
let schema_toml = std::fs::read_to_string(root.join("api-schema.toml"))
|
||||||
|
.context("reading api-schema.toml")?;
|
||||||
|
let doc_md =
|
||||||
|
std::fs::read_to_string(root.join("Documentation.md")).context("reading Documentation.md")?;
|
||||||
|
|
||||||
|
let report = check(&lua_src, &ipc_src, &schema_toml, &doc_md)?;
|
||||||
|
report.print();
|
||||||
|
Ok(report.is_clean())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo_root() -> Result<PathBuf> {
|
||||||
|
// xtask's own Cargo.toml lives at <repo_root>/xtask/Cargo.toml, so its
|
||||||
|
// parent is the workspace root regardless of the caller's cwd (`cargo
|
||||||
|
// run -p xtask` sets CARGO_MANIFEST_DIR to xtask/, not the invocation dir).
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.map(Path::to_path_buf)
|
||||||
|
.context("xtask has no parent directory (unexpected workspace layout)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schema
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Schema {
|
||||||
|
#[serde(default)]
|
||||||
|
entry: Vec<SchemaEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct SchemaEntry {
|
||||||
|
name: String,
|
||||||
|
kind: String,
|
||||||
|
#[serde(default)]
|
||||||
|
since: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_KINDS: &[&str] = &["lua_function", "lua_table", "ipc_method"];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extraction: breadd/src/lua/mod.rs -> the current bread.* Lua API surface.
|
||||||
|
//
|
||||||
|
// This is deliberately line/substring scanning, not a real Lua or Rust
|
||||||
|
// parser (see api-schema.toml's header comment) — precise enough because
|
||||||
|
// `install_api` and its `install_*_helpers` siblings follow a small, stable
|
||||||
|
// set of textual patterns:
|
||||||
|
//
|
||||||
|
// 1. `bread.set("name", ...)` - a top-level binding (function or,
|
||||||
|
// for the known sub-table variables
|
||||||
|
// below, a table).
|
||||||
|
// 2. `<x>_tbl.set("name", ...)` - a method nested under table `<x>`.
|
||||||
|
// 3. `function _bread.name(` /
|
||||||
|
// `function bread.name(` - a plain-Lua-defined top-level
|
||||||
|
// function (log/warn/error/debounce).
|
||||||
|
// 4. `bread.name = function` - ditto, assignment form
|
||||||
|
// (spawn/wait/wait_any/wait_all).
|
||||||
|
// 5. `bread.workflow = {}` and
|
||||||
|
// `bread.workflow.name = function` - the `bread.workflow` table and its
|
||||||
|
// members.
|
||||||
|
|
||||||
|
/// Sub-table variables built in `install_api` and registered onto the
|
||||||
|
/// `bread` global, mapped to the dotted parent name they hang off.
|
||||||
|
/// Deliberately excludes `module_tbl` / `store_tbl`: those back the
|
||||||
|
/// per-module `M` object returned by `bread.module(...)` (i.e. `M.store.get`
|
||||||
|
/// etc.), which is a different namespace from `bread.*` itself.
|
||||||
|
const TABLE_VARS: &[(&str, &str)] = &[
|
||||||
|
("state_tbl", "state"),
|
||||||
|
("profile_tbl", "profile"),
|
||||||
|
("hyprland_tbl", "hyprland"),
|
||||||
|
("widget_tbl", "widget"),
|
||||||
|
("machine_tbl", "machine"),
|
||||||
|
("fs_tbl", "fs"),
|
||||||
|
("json_tbl", "json"),
|
||||||
|
("bluetooth_tbl", "bluetooth"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Read a bare identifier (`[A-Za-z0-9_]+`) starting at byte offset `start`.
|
||||||
|
fn ident_at(text: &str, start: usize) -> &str {
|
||||||
|
let rest = &text[start..];
|
||||||
|
let end = rest
|
||||||
|
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
|
||||||
|
.unwrap_or(rest.len());
|
||||||
|
&rest[..end]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read up to (not including) the next `"` starting at byte offset `start`.
|
||||||
|
fn ident_upto_quote(text: &str, start: usize) -> &str {
|
||||||
|
let rest = &text[start..];
|
||||||
|
let end = rest.find('"').unwrap_or(rest.len());
|
||||||
|
&rest[..end]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_lua_bindings(src: &str) -> BTreeSet<(String, String)> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
|
||||||
|
// 1. Top-level `bread.set("name", ...)`.
|
||||||
|
for (idx, _) in src.match_indices("bread.set(\"") {
|
||||||
|
let name = ident_upto_quote(src, idx + "bread.set(\"".len());
|
||||||
|
if name.is_empty() || name.starts_with("__") {
|
||||||
|
continue; // internal Rust<->Lua bridge fn (e.g. __log_info), not public API
|
||||||
|
}
|
||||||
|
let is_table = TABLE_VARS.iter().any(|(_, parent)| *parent == name);
|
||||||
|
out.insert((
|
||||||
|
(if is_table { "lua_table" } else { "lua_function" }).to_string(),
|
||||||
|
name.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Nested `<x>_tbl.set("name", ...)` for the known sub-tables.
|
||||||
|
for (var, parent) in TABLE_VARS {
|
||||||
|
let prefix = format!("{var}.set(\"");
|
||||||
|
for (idx, _) in src.match_indices(prefix.as_str()) {
|
||||||
|
let name = ident_upto_quote(src, idx + prefix.len());
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.insert(("lua_function".to_string(), format!("{parent}.{name}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. `function _bread.name(` / `function bread.name(`.
|
||||||
|
for prefix in ["function _bread.", "function bread."] {
|
||||||
|
for (idx, _) in src.match_indices(prefix) {
|
||||||
|
let name = ident_at(src, idx + prefix.len());
|
||||||
|
if !name.is_empty() {
|
||||||
|
out.insert(("lua_function".to_string(), name.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. `bread.name = function` (top-level only; `bread.workflow.*` is
|
||||||
|
// handled separately in step 5 since it's a nested table's members).
|
||||||
|
for (idx, _) in src.match_indices("bread.") {
|
||||||
|
let name_start = idx + "bread.".len();
|
||||||
|
let name = ident_at(src, name_start);
|
||||||
|
if name.is_empty() || name == "workflow" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if src[name_start + name.len()..].trim_start().starts_with("= function") {
|
||||||
|
out.insert(("lua_function".to_string(), name.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. `bread.workflow = {}` and `bread.workflow.name = function`.
|
||||||
|
if src.contains("bread.workflow = {}") {
|
||||||
|
out.insert(("lua_table".to_string(), "workflow".to_string()));
|
||||||
|
}
|
||||||
|
for (idx, _) in src.match_indices("bread.workflow.") {
|
||||||
|
let name_start = idx + "bread.workflow.".len();
|
||||||
|
let name = ident_at(src, name_start);
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if src[name_start + name.len()..].trim_start().starts_with("= function") {
|
||||||
|
out.insert(("lua_function".to_string(), format!("workflow.{name}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Extraction: breadd/src/ipc/mod.rs -> the current IPC method surface.
|
||||||
|
|
||||||
|
fn extract_ipc_methods(src: &str) -> BTreeSet<String> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
|
||||||
|
// `events.subscribe` is special-cased ahead of the dispatch `match`
|
||||||
|
// (it upgrades the connection to a streaming socket instead of
|
||||||
|
// returning a single response), so it never appears as a match arm.
|
||||||
|
if src.contains("req.method == \"events.subscribe\"") {
|
||||||
|
out.insert("events.subscribe".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(match_start) = src.find("match req.method.as_str() {") else {
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track brace depth so a `"literal" => ...` pattern belonging to some
|
||||||
|
// *other*, nested match inside an arm's body (e.g. the `"emit"` arm's
|
||||||
|
// own `match source_str { "terminal" => ..., "git" => ... }`) isn't
|
||||||
|
// mistaken for a top-level IPC method arm. Only depth == 1 (directly
|
||||||
|
// inside the outer `match req.method.as_str() { ... }`) counts.
|
||||||
|
let mut depth: i32 = 0;
|
||||||
|
for line in src[match_start..].lines() {
|
||||||
|
let pre_depth = depth;
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
|
||||||
|
if pre_depth == 1 {
|
||||||
|
if let Some(rest) = trimmed.strip_prefix('"') {
|
||||||
|
if let Some(end) = rest.find('"') {
|
||||||
|
let name = &rest[..end];
|
||||||
|
if rest[end + 1..].trim_start().starts_with("=>") {
|
||||||
|
out.insert(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depth += line.matches('{').count() as i32;
|
||||||
|
depth -= line.matches('}').count() as i32;
|
||||||
|
if pre_depth >= 1 && depth <= 0 {
|
||||||
|
break; // closed the outer match block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Documentation.md cross-checks
|
||||||
|
|
||||||
|
/// Slice out a `## `-level section (from its heading up to, but not
|
||||||
|
/// including, the next `## `-level heading).
|
||||||
|
fn section<'a>(doc: &'a str, heading: &str) -> &'a str {
|
||||||
|
let Some(start) = doc.find(heading) else {
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
let rest = &doc[start..];
|
||||||
|
match rest[3..].find("\n## ") {
|
||||||
|
Some(i) => &rest[..i + 3],
|
||||||
|
None => rest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lua_api_section(doc: &str) -> &str {
|
||||||
|
section(doc, "## Dictionary: Lua API")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ipc_section(doc: &str) -> &str {
|
||||||
|
section(doc, "## Dictionary: IPC protocol")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Report
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct CheckReport {
|
||||||
|
/// In code, not in api-schema.toml.
|
||||||
|
missing_from_schema: Vec<(String, String)>,
|
||||||
|
/// In api-schema.toml, no longer in code.
|
||||||
|
stale_in_schema: Vec<(String, String)>,
|
||||||
|
/// In api-schema.toml (and in code), but Documentation.md has no
|
||||||
|
/// heading/row for it.
|
||||||
|
undocumented: Vec<(String, String)>,
|
||||||
|
/// Schema entries with an unrecognized `kind`.
|
||||||
|
bad_kind: Vec<(String, String)>,
|
||||||
|
/// Schema entries with an empty `since`.
|
||||||
|
missing_since: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheckReport {
|
||||||
|
fn is_clean(&self) -> bool {
|
||||||
|
self.missing_from_schema.is_empty()
|
||||||
|
&& self.stale_in_schema.is_empty()
|
||||||
|
&& self.undocumented.is_empty()
|
||||||
|
&& self.bad_kind.is_empty()
|
||||||
|
&& self.missing_since.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print(&self) {
|
||||||
|
if self.is_clean() {
|
||||||
|
println!("check-docs: OK — api-schema.toml matches breadd's Lua/IPC surface and Documentation.md.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.bad_kind.is_empty() {
|
||||||
|
println!("Schema entries with an unrecognized `kind` (expected one of {VALID_KINDS:?}):");
|
||||||
|
for (kind, name) in &self.bad_kind {
|
||||||
|
println!(" - {name} (kind = \"{kind}\")");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.missing_from_schema.is_empty() {
|
||||||
|
println!("Added but undocumented in api-schema.toml (present in code, missing from schema):");
|
||||||
|
for (kind, name) in &self.missing_from_schema {
|
||||||
|
println!(" - [{kind}] {name}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.stale_in_schema.is_empty() {
|
||||||
|
println!("Stale in api-schema.toml (no longer found in code):");
|
||||||
|
for (kind, name) in &self.stale_in_schema {
|
||||||
|
println!(" - [{kind}] {name}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.undocumented.is_empty() {
|
||||||
|
println!("In api-schema.toml but missing from Documentation.md (no `#### bread.<name>` heading for a lua_function/lua_table, or no `<name>` row in the IPC Methods table):");
|
||||||
|
for (kind, name) in &self.undocumented {
|
||||||
|
println!(" - [{kind}] {name}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.missing_since.is_empty() {
|
||||||
|
println!("Schema entries with an empty `since`:");
|
||||||
|
for (kind, name) in &self.missing_since {
|
||||||
|
println!(" - [{kind}] {name}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("check-docs: FAILED — see above.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(lua_src: &str, ipc_src: &str, schema_toml: &str, doc_md: &str) -> Result<CheckReport> {
|
||||||
|
let schema: Schema = toml::from_str(schema_toml).context("parsing api-schema.toml")?;
|
||||||
|
|
||||||
|
let mut report = CheckReport::default();
|
||||||
|
|
||||||
|
let mut schema_set: BTreeSet<(String, String)> = BTreeSet::new();
|
||||||
|
for entry in &schema.entry {
|
||||||
|
if !VALID_KINDS.contains(&entry.kind.as_str()) {
|
||||||
|
report.bad_kind.push((entry.kind.clone(), entry.name.clone()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if entry.since.trim().is_empty() {
|
||||||
|
report
|
||||||
|
.missing_since
|
||||||
|
.push((entry.kind.clone(), entry.name.clone()));
|
||||||
|
}
|
||||||
|
schema_set.insert((entry.kind.clone(), entry.name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- code vs schema: Lua ---
|
||||||
|
let code_lua = extract_lua_bindings(lua_src);
|
||||||
|
let schema_lua: BTreeSet<(String, String)> = schema_set
|
||||||
|
.iter()
|
||||||
|
.filter(|(kind, _)| kind != "ipc_method")
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for entry in code_lua.difference(&schema_lua) {
|
||||||
|
report.missing_from_schema.push(entry.clone());
|
||||||
|
}
|
||||||
|
for entry in schema_lua.difference(&code_lua) {
|
||||||
|
report.stale_in_schema.push(entry.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- code vs schema: IPC ---
|
||||||
|
let code_ipc = extract_ipc_methods(ipc_src);
|
||||||
|
let schema_ipc: BTreeSet<String> = schema_set
|
||||||
|
.iter()
|
||||||
|
.filter(|(kind, _)| kind == "ipc_method")
|
||||||
|
.map(|(_, name)| name.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for name in code_ipc.difference(&schema_ipc) {
|
||||||
|
report
|
||||||
|
.missing_from_schema
|
||||||
|
.push(("ipc_method".to_string(), name.clone()));
|
||||||
|
}
|
||||||
|
for name in schema_ipc.difference(&code_ipc) {
|
||||||
|
report
|
||||||
|
.stale_in_schema
|
||||||
|
.push(("ipc_method".to_string(), name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- schema vs Documentation.md ---
|
||||||
|
let lua_section = lua_api_section(doc_md);
|
||||||
|
let ipc_tbl_section = ipc_section(doc_md);
|
||||||
|
for entry in &schema.entry {
|
||||||
|
if !VALID_KINDS.contains(&entry.kind.as_str()) {
|
||||||
|
continue; // already reported above
|
||||||
|
}
|
||||||
|
let documented = if entry.kind == "ipc_method" {
|
||||||
|
ipc_tbl_section.contains(&format!("`{}`", entry.name))
|
||||||
|
} else {
|
||||||
|
lua_section.contains(&format!("bread.{}", entry.name))
|
||||||
|
};
|
||||||
|
if !documented {
|
||||||
|
report
|
||||||
|
.undocumented
|
||||||
|
.push((entry.kind.clone(), entry.name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report.missing_from_schema.sort();
|
||||||
|
report.stale_in_schema.sort();
|
||||||
|
report.undocumented.sort();
|
||||||
|
report.bad_kind.sort();
|
||||||
|
report.missing_since.sort();
|
||||||
|
|
||||||
|
Ok(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const LUA_SRC: &str = r#"
|
||||||
|
bread.set("on", on_fn)?;
|
||||||
|
bread.set("state", state_tbl)?;
|
||||||
|
state_tbl.set("get", get_fn)?;
|
||||||
|
state_tbl.set("watch", watch_fn)?;
|
||||||
|
bread.set("__log_info", info_fn)?;
|
||||||
|
function bread.debounce(delay_ms, fn)
|
||||||
|
bread.spawn = function(fn)
|
||||||
|
bread.workflow = {}
|
||||||
|
bread.workflow.define = function(name, fn)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const IPC_SRC: &str = r#"
|
||||||
|
if req.method == "events.subscribe" {
|
||||||
|
}
|
||||||
|
let result = match req.method.as_str() {
|
||||||
|
"ping" => Ok(json!({ "ok": true })),
|
||||||
|
"emit" => {
|
||||||
|
let source = match source_str {
|
||||||
|
"terminal" => AdapterSource::Terminal,
|
||||||
|
"git" => AdapterSource::Git,
|
||||||
|
other => AdapterSource::App(other.to_string()),
|
||||||
|
};
|
||||||
|
Ok(json!({ "emitted": true }))
|
||||||
|
}
|
||||||
|
_ => Err("unknown method".to_string()),
|
||||||
|
};
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const DOC_MD: &str = r#"
|
||||||
|
## Dictionary: Lua API
|
||||||
|
|
||||||
|
#### `bread.on(pattern, fn) -> id`
|
||||||
|
#### `bread.state.get(path)`
|
||||||
|
#### `bread.state.watch(path, fn) -> id`
|
||||||
|
#### `bread.debounce(delay_ms, fn) -> wrapped_fn`
|
||||||
|
#### `bread.spawn(fn)`
|
||||||
|
### Workflows
|
||||||
|
#### `bread.workflow.define(name, fn)`
|
||||||
|
|
||||||
|
## Dictionary: IPC protocol
|
||||||
|
|
||||||
|
| Method | Params | Description |
|
||||||
|
|--------|--------|-------------|
|
||||||
|
| `ping` | - | Connectivity check |
|
||||||
|
| `emit` | - | Inject an event |
|
||||||
|
| `events.subscribe` | - | Upgrade to streaming mode |
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn schema_toml_for(entries: &[(&str, &str, &str)]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for (name, kind, since) in entries {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"[[entry]]\nname = \"{name}\"\nkind = \"{kind}\"\nsince = \"{since}\"\n\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_schema() -> String {
|
||||||
|
schema_toml_for(&[
|
||||||
|
("on", "lua_function", "1.0"),
|
||||||
|
("state", "lua_table", "1.0"),
|
||||||
|
("state.get", "lua_function", "1.0"),
|
||||||
|
("state.watch", "lua_function", "1.0"),
|
||||||
|
("debounce", "lua_function", "1.0"),
|
||||||
|
("spawn", "lua_function", "1.0"),
|
||||||
|
("workflow", "lua_table", "1.2"),
|
||||||
|
("workflow.define", "lua_function", "1.2"),
|
||||||
|
("ping", "ipc_method", "1.0"),
|
||||||
|
("emit", "ipc_method", "1.0"),
|
||||||
|
("events.subscribe", "ipc_method", "1.0"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_lua_bindings_precisely() {
|
||||||
|
let got = extract_lua_bindings(LUA_SRC);
|
||||||
|
let want: BTreeSet<(String, String)> = [
|
||||||
|
("lua_function", "on"),
|
||||||
|
("lua_table", "state"),
|
||||||
|
("lua_function", "state.get"),
|
||||||
|
("lua_function", "state.watch"),
|
||||||
|
("lua_function", "debounce"),
|
||||||
|
("lua_function", "spawn"),
|
||||||
|
("lua_table", "workflow"),
|
||||||
|
("lua_function", "workflow.define"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, n)| (k.to_string(), n.to_string()))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(got, want, "must exclude __-prefixed internals");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extracts_ipc_methods_without_leaking_nested_match_arms() {
|
||||||
|
let got = extract_ipc_methods(IPC_SRC);
|
||||||
|
let want: BTreeSet<String> = ["events.subscribe", "ping", "emit"]
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
got, want,
|
||||||
|
"must not pick up \"terminal\"/\"git\" from the nested `match source_str` inside the emit arm"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clean_state_passes() {
|
||||||
|
let report = check(LUA_SRC, IPC_SRC, &full_schema(), DOC_MD).unwrap();
|
||||||
|
assert!(report.is_clean(), "{report:#?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renamed_schema_entry_is_caught_as_drift() {
|
||||||
|
// Simulate a contributor renaming `bread.debounce` in code without
|
||||||
|
// updating api-schema.toml: schema still says "debounce", code no
|
||||||
|
// longer does (only "debounce_v2" now exists).
|
||||||
|
let renamed_lua_src = LUA_SRC.replace("bread.debounce", "bread.debounce_v2");
|
||||||
|
let report = check(&renamed_lua_src, IPC_SRC, &full_schema(), DOC_MD).unwrap();
|
||||||
|
assert!(!report.is_clean());
|
||||||
|
assert!(report
|
||||||
|
.missing_from_schema
|
||||||
|
.contains(&("lua_function".to_string(), "debounce_v2".to_string())));
|
||||||
|
assert!(report
|
||||||
|
.stale_in_schema
|
||||||
|
.contains(&("lua_function".to_string(), "debounce".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removed_ipc_method_is_caught_as_drift() {
|
||||||
|
let without_emit = IPC_SRC.replacen("\"emit\" => {", "\"emit_removed\" => {", 1);
|
||||||
|
let report = check(LUA_SRC, &without_emit, &full_schema(), DOC_MD).unwrap();
|
||||||
|
assert!(!report.is_clean());
|
||||||
|
assert!(report
|
||||||
|
.stale_in_schema
|
||||||
|
.contains(&("ipc_method".to_string(), "emit".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_doc_heading_is_caught_as_drift() {
|
||||||
|
let doc_without_spawn_heading = DOC_MD.replace("#### `bread.spawn(fn)`\n", "");
|
||||||
|
let report = check(LUA_SRC, IPC_SRC, &full_schema(), &doc_without_spawn_heading).unwrap();
|
||||||
|
assert!(!report.is_clean());
|
||||||
|
assert!(report
|
||||||
|
.undocumented
|
||||||
|
.contains(&("lua_function".to_string(), "spawn".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_kind_is_rejected() {
|
||||||
|
let bad_schema = schema_toml_for(&[("on", "lua_thing", "1.0")]);
|
||||||
|
let report = check(LUA_SRC, IPC_SRC, &bad_schema, DOC_MD).unwrap();
|
||||||
|
assert!(!report.is_clean());
|
||||||
|
assert!(report
|
||||||
|
.bad_kind
|
||||||
|
.contains(&("lua_thing".to_string(), "on".to_string())));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue