From 62504dda1527490cb49e56dd9840031479d258f0 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sun, 20 Sep 2026 22:25:07 +0200 Subject: docs: generate the command catalogue from the command tables Co-Authored-By: Claude Opus 5 --- Makefile | 18 +++- docs/PROTOCOL.md | 99 +++++++++++++++++ scripts/check-consistency.sh | 7 +- scripts/gen_protocol.c | 249 +++++++++++++++++++++++++++++++++++++++++++ src/commands.c | 69 ++++++------ src/commands.h | 1 + 6 files changed, 408 insertions(+), 35 deletions(-) create mode 100644 scripts/gen_protocol.c diff --git a/Makefile b/Makefile index dfdac40..43b5417 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,12 @@ $(BUILD)/test_smtp: $(BUILD)/tests/smtp_check.o $(BUILD)/src/smtp.o \ $(BUILD)/vendor/sha256.o $(CC) $(CFLAGS) -o $@ $^ -lm -lssl -lcrypto +$(BUILD)/gen_protocol: $(BUILD)/scripts/gen_protocol.o $(CORE_OBJ) $(VENDOR_OBJ) + $(CC) $(CFLAGS) -o $@ $^ -lm $(SSL_LIBS) + +gen-protocol: $(BUILD)/gen_protocol + $(BUILD)/gen_protocol --write docs/PROTOCOL.md + test: check $(BUILD)/test_core $(BUILD)/test_tui $(BUILD)/test_pdf \ $(BUILD)/test_invoice $(BUILD)/test_smtp $(BUILD)/test_core @@ -96,7 +102,8 @@ test: check $(BUILD)/test_core $(BUILD)/test_tui $(BUILD)/test_pdf \ test-core: $(BUILD)/test_core $(BUILD)/test_core -check: +check: $(BUILD)/gen_protocol + $(BUILD)/gen_protocol --check docs/PROTOCOL.md sh scripts/check-consistency.sh test-pty: all @@ -127,6 +134,10 @@ $(BUILD)/clients/%.o: clients/%.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(DEPFLAGS) $(WARN) $(CPPFLAGS) $(DEFS) -c $< -o $@ +$(BUILD)/scripts/%.o: scripts/%.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(DEPFLAGS) $(WARN) $(CPPFLAGS) $(DEFS) -c $< -o $@ + $(BUILD)/tests/%.o: tests/%.c @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(DEPFLAGS) $(WARN) $(CPPFLAGS) $(DEFS) -c $< -o $@ @@ -135,7 +146,8 @@ $(BUILD)/tests/%.o: tests/%.c $(BUILD)/clients/bokfctl.d $(BUILD)/clients/bokftui.d \ $(BUILD)/clients/ui.d $(TUI_SCREEN_DEP) \ $(BUILD)/clients/tui.d $(BUILD)/clients/client.d \ - $(BUILD)/tests/test_core.d $(BUILD)/tests/test_tui.d + $(BUILD)/tests/test_core.d $(BUILD)/tests/test_tui.d \ + $(BUILD)/scripts/gen_protocol.d install: all install -d $(DESTDIR)/usr/local/bin $(DESTDIR)/usr/local/share/bokf @@ -146,5 +158,5 @@ install: all clean: rm -rf $(BUILD) build-asan build-ubsan -.PHONY: all backend test test-core test-pty check test-asan test-ubsan gate \ +.PHONY: all backend test test-core test-pty check gen-protocol test-asan test-ubsan gate \ install clean diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index bbe8166..5aa4ff6 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -644,6 +644,105 @@ updated and the `invoice.send` audit entry stores `{id,to,subject}` only. `dry_run` validates configuration, recipient and stored document and returns the recipient and subject without sending or updating anything. + +## Command catalogue (generated) + +Generated by `make gen-protocol` from the command tables; do not edit by hand. +Args: `name:type[!][=default][ enum a\|b]`, `!` = required. + +| Command | Permission | Org | Mutating | Dry run | Args | +|---|---|---|---|---|---| +| `health` | public | no | no | no | — | +| `meta` | public | no | no | no | — | +| `session.open` | public | no | no | no | `method:enum! enum password\|token`, `username:string`, `password:string`, `token:string` | +| `session.close` | viewer | no | no | no | — | +| `session.whoami` | viewer | no | no | no | — | +| `session.list_orgs` | viewer | no | no | no | — | +| `session.use_org` | viewer | no | no | no | `org:int!` | +| `org.create` | viewer | no | yes | yes | `name:string!`, `org_nr:string`, `fiscal_year_start_month:int=1`, `moms_period:enum=month enum month\|quarter\|year`, `framework:enum=K2 enum K2\|K3` | +| `org.list` | viewer | no | no | no | — | +| `org.get` | viewer | yes | no | no | — | +| `org.update` | owner | yes | yes | yes | `name:string`, `org_nr:string`, `vat_nr:string`, `address:string`, `postal_code:string`, `city:string`, `country:string`, `email:string`, `phone:string`, `moms_period:enum enum month\|quarter\|year`, `framework:enum enum K2\|K3`, `description:string`, `fiscal_year_start_month:int`, `shares:int` | +| `org.member_list` | viewer | yes | no | no | — | +| `org.member_add` | owner | yes | yes | yes | `username:string!`, `role:enum! enum owner\|bookkeeper\|viewer` | +| `org.member_set_role` | owner | yes | yes | yes | `username:string!`, `role:enum! enum owner\|bookkeeper\|viewer` | +| `org.member_remove` | owner | yes | yes | yes | `username:string!` | +| `board.list` | viewer | yes | no | no | — | +| `board.add` | owner | yes | yes | yes | `name:string!`, `title:string=Styrelseledamot` | +| `board.update` | owner | yes | yes | yes | `id:int!`, `name:string`, `title:string` | +| `board.remove` | owner | yes | yes | yes | `id:int!` | +| `user.create` | admin | no | yes | yes | `username:string!`, `password:string!`, `display_name:string`, `is_admin:bool=false` | +| `user.list` | admin | no | no | no | — | +| `token.create` | viewer | yes | yes | yes | `label:string!`, `scopes:json`, `expires_at:date` | +| `token.list` | viewer | yes | no | no | — | +| `token.revoke` | viewer | yes | yes | yes | `id:int!` | +| `describe` | viewer | no | no | no | `cmd:string` | +| `agent.instructions` | viewer | no | no | no | — | +| `audit.list` | viewer | yes | no | no | `cursor:int`, `limit:int=100`, `action:string` | +| `audit.verify` | viewer | no | no | no | `full:bool=false` | +| `backup.snapshot` | admin | no | yes | no | `dest:string` | +| `account.list` | viewer | yes | no | no | `active_only:bool` | +| `account.get` | viewer | yes | no | no | `id:int`, `number:string` | +| `account.create` | bookkeeper | yes | yes | yes | `number:string!`, `name:string!`, `type:enum! enum asset\|liability\|equity\|revenue\|expense`, `sru_code:string`, `vat_code:string` | +| `account.update` | bookkeeper | yes | yes | yes | `id:int!`, `name:string`, `sru_code:string`, `vat_code:string`, `active:bool` | +| `fiscal_year.list` | viewer | yes | no | no | — | +| `fiscal_year.get` | viewer | yes | no | no | `id:int` | +| `fiscal_year.open` | owner | yes | yes | yes | `label:string!`, `start_date:date!`, `end_date:date!` | +| `fiscal_year.close` | owner | yes | yes | no | `id:int!`, `confirm:bool!` | +| `fiscal_year.reopen` | owner | yes | yes | yes | `id:int!`, `confirm:bool!` | +| `fiscal_year.update` | bookkeeper | yes | yes | yes | `id:int!`, `dividend_ore:int`, `events:string`, `agm_date:string`, `dividend_date:string`, `employees:string`, `notes:string` | +| `period.lock` | owner | yes | yes | yes | `fiscal_year:int!`, `until:date!`, `reason:string` | +| `period.unlock` | owner | yes | yes | yes | `fiscal_year:int!`, `reason:string` | +| `voucher.post` | bookkeeper | yes | yes | yes | `date:date!`, `description:string`, `series:string`, `client_ref:string`, `corrects_voucher:int`, `rows:json`, `template:json`, `x:json`, `attachment_ids:json` | +| `voucher.get` | viewer | yes | no | no | `id:int!` | +| `voucher.list` | viewer | yes | no | no | `fiscal_year:int`, `from:date`, `to:date`, `series:string`, `account:string`, `text:string`, `cursor:int`, `limit:int=100` | +| `voucher.correct` | bookkeeper | yes | yes | yes | `voucher:int!`, `description:string!`, `date:date`, `client_ref:string` | +| `bokslut.post` | bookkeeper | yes | yes | yes | `fiscal_year:int`, `entries:json`, `periodiseringsfond_ore:int`, `tax_rate:json=20.6`, `dispose:bool=true`, `date:date` | +| `settings.get` | viewer | yes | no | no | — | +| `settings.set` | bookkeeper | yes | yes | yes | `key:string!`, `value:string` | +| `bank.import` | bookkeeper | yes | yes | yes | `format:enum! enum seb`, `content_base64:string`, `path:string`, `account:string` | +| `bank.list` | viewer | yes | no | no | `status:enum=all enum all\|unmatched\|matched`, `from:date`, `to:date`, `account:string`, `limit:int=200` | +| `bank.match` | bookkeeper | yes | yes | yes | `transaction_id:int!`, `voucher_id:int!` | +| `bank.unmatch` | bookkeeper | yes | yes | yes | `transaction_id:int!`, `voucher_id:int!` | +| `report.rule_list` | viewer | yes | no | no | `report:enum enum vat` | +| `report.rule_create` | owner | yes | yes | yes | `report:enum! enum vat`, `box:string!`, `match_type:enum! enum account\|range\|type`, `pattern:string!`, `sign:int=1`, `sort_order:int=0` | +| `report.rule_update` | owner | yes | yes | yes | `id:int!`, `box:string`, `match_type:enum enum account\|range\|type`, `pattern:string`, `sign:int`, `sort_order:int` | +| `report.rule_delete` | owner | yes | yes | yes | `id:int!` | +| `template.list` | viewer | yes | no | no | `active_only:bool` | +| `template.get` | viewer | yes | no | no | `id:int`, `name:string` | +| `template.create` | bookkeeper | yes | yes | yes | `name:string!`, `series:string`, `description:string`, `rows:json!` | +| `template.update` | bookkeeper | yes | yes | yes | `id:int`, `name:string`, `series:string`, `description:string`, `active:bool`, `rows:json` | +| `template.archive` | bookkeeper | yes | yes | yes | `id:int`, `name:string` | +| `attachment.put` | bookkeeper | yes | yes | yes | `filename:string!`, `mime:string=application/octet-stream`, `content_base64:string!`, `voucher_id:int` | +| `attachment.link` | bookkeeper | yes | yes | yes | `id:int!`, `voucher_id:int!` | +| `attachment.unlink` | bookkeeper | yes | yes | yes | `id:int!`, `voucher_id:int!` | +| `attachment.get` | viewer | yes | no | no | `id:int!` | +| `attachment.list` | viewer | yes | no | no | `voucher_id:int`, `unlinked:bool`, `cursor:int`, `limit:int=100` | +| `report.trial_balance` | viewer | yes | no | no | `fiscal_year:int`, `from:date`, `to:date`, `include_zero:bool=false` | +| `report.income_statement` | viewer | yes | no | no | `fiscal_year:int`, `from:date`, `to:date` | +| `report.balance_sheet` | viewer | yes | no | no | `fiscal_year:int`, `to:date` | +| `report.vat` | viewer | yes | no | no | `from:date!`, `to:date!` | +| `report.general_ledger` | viewer | yes | no | no | `fiscal_year:int`, `accounts:json`, `from:date`, `to:date` | +| `report.voucher_list` | viewer | yes | no | no | `fiscal_year:int`, `series:string` | +| `report.vat_eskd` | viewer | yes | no | no | `from:date!`, `to:date!`, `upplysning:string` | +| `sru.export` | viewer | yes | no | no | `fiscal_year:int`, `adjustments:json`, `submitter:json`, `assisted:bool`, `audited:bool`, `ignore_unmapped:bool=false` | +| `sie.export` | viewer | yes | no | no | `fiscal_year:int`, `inline:bool=false` | +| `sie.import` | bookkeeper | yes | yes | yes | `content_base64:string`, `path:string` | +| `customer.list` | viewer | yes | no | no | `active_only:bool` | +| `customer.get` | viewer | yes | no | no | `id:int!` | +| `customer.create` | bookkeeper | yes | yes | yes | `name:string!`, `address:string`, `postal_code:string`, `city:string`, `country:string=SE`, `vat_nr:string`, `email:string`, `your_ref:string`, `notes:string`, `payment_days:int=30` | +| `customer.update` | bookkeeper | yes | yes | yes | `id:int!`, `name:string`, `address:string`, `postal_code:string`, `city:string`, `country:string`, `vat_nr:string`, `email:string`, `your_ref:string`, `notes:string`, `payment_days:int`, `active:bool` | +| `customer.archive` | bookkeeper | yes | yes | yes | `id:int!`, `active:bool!` | +| `invoice.sequence_get` | viewer | yes | no | no | — | +| `invoice.sequence_set` | owner | yes | yes | yes | `next_number:int!` | +| `invoice.preview` | viewer | yes | no | no | `customer_id:int!`, `invoice_date:date!`, `due_date:date!`, `delivery_date:string`, `your_ref:string`, `our_ref:string`, `notes:string`, `rows:json!` | +| `invoice.issue` | bookkeeper | yes | yes | yes | `customer_id:int!`, `invoice_date:date!`, `due_date:date!`, `delivery_date:string`, `your_ref:string`, `our_ref:string`, `notes:string`, `rows:json!` | +| `invoice.get` | viewer | yes | no | no | `id:int!` | +| `invoice.list` | viewer | yes | no | no | `customer_id:int`, `status:enum enum issued\|credited`, `limit:int=200` | +| `invoice.pdf` | viewer | yes | no | no | `id:int!` | +| `invoice.send` | bookkeeper | yes | yes | yes | `id:int!`, `to:string` | + + ## 8. The TUI is just a client `bokftui` logs in over the same socket, picks an org and issues the same diff --git a/scripts/check-consistency.sh b/scripts/check-consistency.sh index 600db25..3ca9b0c 100755 --- a/scripts/check-consistency.sh +++ b/scripts/check-consistency.sh @@ -70,8 +70,13 @@ fi # --- extract what the docs promise ----------------------------------------- # Only rows of tables whose header first cell is "Command" count; prose, -# argument names and other tables must not produce command names. +# argument names and other tables must not produce command names. The +# generated catalogue (gen_protocol) is skipped so every command still needs +# a hand-written table row with its result. awk ' + // { skip = 1 } + // { skip = 0; next } + skip { next } /^\|/ { n = split($0, cells, "|") cell = cells[2] diff --git a/scripts/gen_protocol.c b/scripts/gen_protocol.c new file mode 100644 index 0000000..b39bb7d --- /dev/null +++ b/scripts/gen_protocol.c @@ -0,0 +1,249 @@ +/* Emits the generated command catalogue for docs/PROTOCOL.md from the + * command tables. Usage: gen_protocol --stdout | --write FILE | --check FILE */ +#include +#include +#include +#include + +#include "commands.h" +#include "util.h" +#include "yyjson.h" + +#define BEGIN_MARK "" +#define END_MARK "" +#define INSERT_BEFORE "\n## 8. " + +static void buf_put(struct buf *b, const char *s, size_t n) +{ + buf_append(b, s, n); +} + +static char *buf_take(struct buf *b) +{ + buf_append(b, "", 1); + return (char *)b->p; +} + +static void buf_puts(struct buf *b, const char *s) +{ + buf_put(b, s, strlen(s)); +} + +static void buf_cell(struct buf *b, const char *s) +{ + for (; *s; s++) { + if (*s == '|') + buf_puts(b, "\\|"); + else + buf_put(b, s, 1); + } +} + +static const char *jstr(yyjson_val *o, const char *k) +{ + yyjson_val *v = yyjson_obj_get(o, k); + return v && yyjson_is_str(v) ? yyjson_get_str(v) : ""; +} + +static int jbool(yyjson_val *o, const char *k) +{ + yyjson_val *v = yyjson_obj_get(o, k); + return v && yyjson_get_bool(v); +} + +static void render_args(struct buf *b, yyjson_val *args) +{ + size_t idx, max; + yyjson_val *a; + if (!yyjson_is_arr(args) || yyjson_arr_size(args) == 0) { + buf_puts(b, "—"); + return; + } + yyjson_arr_foreach(args, idx, max, a) { + if (idx) + buf_puts(b, ", "); + buf_puts(b, "`"); + buf_cell(b, jstr(a, "name")); + buf_puts(b, ":"); + buf_cell(b, jstr(a, "type")); + if (jbool(a, "required")) + buf_puts(b, "!"); + yyjson_val *def = yyjson_obj_get(a, "default"); + if (def) { + buf_puts(b, "="); + buf_cell(b, yyjson_get_str(def)); + } + yyjson_val *vals = yyjson_obj_get(a, "values"); + if (vals && yyjson_is_arr(vals)) { + size_t vi, vmax; + yyjson_val *v; + buf_puts(b, " enum "); + yyjson_arr_foreach(vals, vi, vmax, v) { + if (vi) + buf_puts(b, "\\|"); + buf_cell(b, yyjson_get_str(v)); + } + } + buf_puts(b, "`"); + } +} + +static char *render_block(void) +{ + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *cmds = commands_describe(doc); + yyjson_mut_doc_set_root(doc, cmds); + char *json = yyjson_mut_write(doc, 0, NULL); + yyjson_mut_doc_free(doc); + yyjson_doc *rd = yyjson_read(json, strlen(json), 0); + free(json); + + struct buf b; + buf_init(&b); + buf_puts(&b, BEGIN_MARK "\n"); + buf_puts(&b, "## Command catalogue (generated)\n\n"); + buf_puts(&b, "Generated by `make gen-protocol` from the command tables; do not " + "edit by hand.\nArgs: `name:type[!][=default][ enum a\\|b]`, `!` = " + "required.\n\n"); + buf_puts(&b, "| Command | Permission | Org | Mutating | Dry run | Args |\n"); + buf_puts(&b, "|---|---|---|---|---|---|\n"); + size_t idx, max; + yyjson_val *c; + yyjson_arr_foreach(yyjson_doc_get_root(rd), idx, max, c) { + yyjson_val *perm = yyjson_obj_get(c, "permission"); + buf_puts(&b, "| `"); + buf_cell(&b, jstr(c, "name")); + buf_puts(&b, "` | "); + buf_cell(&b, jstr(perm, "role")); + buf_puts(&b, " | "); + buf_puts(&b, jbool(perm, "require_org") ? "yes" : "no"); + buf_puts(&b, " | "); + buf_puts(&b, jbool(c, "mutating") ? "yes" : "no"); + buf_puts(&b, " | "); + buf_puts(&b, jbool(c, "dry_run") ? "yes" : "no"); + buf_puts(&b, " | "); + render_args(&b, yyjson_obj_get(c, "args")); + buf_puts(&b, " |\n"); + } + buf_puts(&b, END_MARK "\n"); + yyjson_doc_free(rd); + return buf_take(&b); +} + +static char *read_all(const char *path, size_t *n) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + perror(path); + exit(2); + } + struct buf b; + buf_init(&b); + char tmp[8192]; + size_t k; + while ((k = fread(tmp, 1, sizeof tmp, f)) > 0) + buf_put(&b, tmp, k); + fclose(f); + *n = b.len; + return buf_take(&b); +} + +static char *splice(const char *text, const char *block, char **old_out) +{ + const char *begin = strstr(text, BEGIN_MARK); + const char *end = begin ? strstr(begin, END_MARK) : NULL; + struct buf b; + buf_init(&b); + if (begin && end) { + end += strlen(END_MARK); + if (*end == '\n') + end++; + size_t oldn = (size_t)(end - begin); + *old_out = xmalloc(oldn + 1); + memcpy(*old_out, begin, oldn); + (*old_out)[oldn] = '\0'; + buf_put(&b, text, (size_t)(begin - text)); + buf_puts(&b, block); + buf_puts(&b, end); + return buf_take(&b); + } + if (begin || end) { + fprintf(stderr, "gen_protocol: unbalanced generated markers\n"); + exit(2); + } + *old_out = xstrdup(""); + const char *at = strstr(text, INSERT_BEFORE); + if (!at) { + fprintf(stderr, "gen_protocol: no '%s' section to insert before\n", + INSERT_BEFORE + 1); + exit(2); + } + at++; + buf_put(&b, text, (size_t)(at - text)); + buf_puts(&b, block); + buf_puts(&b, "\n"); + buf_puts(&b, at); + return buf_take(&b); +} + +static void print_diff(const char *old, const char *new) +{ + char oldp[] = "/tmp/gen_protocol-old-XXXXXX"; + char newp[] = "/tmp/gen_protocol-new-XXXXXX"; + int fo = mkstemp(oldp), fn = mkstemp(newp); + if (fo < 0 || fn < 0) + return; + FILE *a = fdopen(fo, "w"), *c = fdopen(fn, "w"); + fputs(old, a); + fputs(new, c); + fclose(a); + fclose(c); + char cmd[256]; + snprintf(cmd, sizeof cmd, "diff -u --label committed --label generated %s %s >&2", + oldp, newp); + if (system(cmd) < 0) + fprintf(stderr, "gen_protocol: diff failed\n"); + unlink(oldp); + unlink(newp); +} + +int main(int argc, char **argv) +{ + if (argc == 2 && strcmp(argv[1], "--stdout") == 0) { + char *block = render_block(); + fputs(block, stdout); + free(block); + return 0; + } + if (argc != 3 || (strcmp(argv[1], "--write") != 0 && + strcmp(argv[1], "--check") != 0)) { + fprintf(stderr, "usage: gen_protocol --stdout | --write FILE | --check FILE\n"); + return 2; + } + size_t n; + char *text = read_all(argv[2], &n); + char *block = render_block(); + char *old = NULL; + char *out = splice(text, block, &old); + int same = strcmp(old, block) == 0; + if (strcmp(argv[1], "--check") == 0) { + if (!same) { + fprintf(stderr, "gen_protocol: %s command catalogue is stale; " + "run 'make gen-protocol'\n", argv[2]); + print_diff(old, block); + } + } else if (!same) { + FILE *f = fopen(argv[2], "wb"); + if (!f) { + perror(argv[2]); + return 2; + } + fputs(out, f); + fclose(f); + } + free(text); + free(block); + free(old); + free(out); + return same || strcmp(argv[1], "--write") == 0 ? 0 : 1; +} diff --git a/src/commands.c b/src/commands.c index fe97afa..f30e82a 100644 --- a/src/commands.c +++ b/src/commands.c @@ -10,52 +10,64 @@ /* discovery */ /* ------------------------------------------------------------------ */ -static yyjson_mut_val *command_json(struct req *r, const struct command *c) +static yyjson_mut_val *command_json(yyjson_mut_doc *doc, + const struct command *c) { - yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); - yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", c->name); - yyjson_mut_obj_add_strcpy(r->rdoc, o, "summary", c->summary); - yyjson_mut_obj_add_bool(r->rdoc, o, "mutating", c->mutating != 0); - yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", c->dry_run != 0); - yyjson_mut_val *perm = yyjson_mut_obj(r->rdoc); - yyjson_mut_obj_add_strcpy(r->rdoc, perm, "role", perm_name(c->perm)); + yyjson_mut_val *o = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, o, "name", c->name); + yyjson_mut_obj_add_strcpy(doc, o, "summary", c->summary); + yyjson_mut_obj_add_bool(doc, o, "mutating", c->mutating != 0); + yyjson_mut_obj_add_bool(doc, o, "dry_run", c->dry_run != 0); + yyjson_mut_val *perm = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strcpy(doc, perm, "role", perm_name(c->perm)); const char *scope = perm_scope(c->perm); if (scope) - yyjson_mut_obj_add_strcpy(r->rdoc, perm, "scope", scope); + yyjson_mut_obj_add_strcpy(doc, perm, "scope", scope); else - yyjson_mut_obj_add_null(r->rdoc, perm, "scope"); - yyjson_mut_obj_add_bool(r->rdoc, perm, "require_org", c->need_org != 0); - yyjson_mut_obj_add_val(r->rdoc, o, "permission", perm); - yyjson_mut_val *args = yyjson_mut_arr(r->rdoc); + yyjson_mut_obj_add_null(doc, perm, "scope"); + yyjson_mut_obj_add_bool(doc, perm, "require_org", c->need_org != 0); + yyjson_mut_obj_add_val(doc, o, "permission", perm); + yyjson_mut_val *args = yyjson_mut_arr(doc); for (size_t i = 0; i < c->nargs; i++) { const struct cmd_arg *a = &c->args[i]; - yyjson_mut_val *ao = yyjson_mut_arr_add_obj(r->rdoc, args); - yyjson_mut_obj_add_strcpy(r->rdoc, ao, "name", a->name); - yyjson_mut_obj_add_strcpy(r->rdoc, ao, "type", + yyjson_mut_val *ao = yyjson_mut_arr_add_obj(doc, args); + yyjson_mut_obj_add_strcpy(doc, ao, "name", a->name); + yyjson_mut_obj_add_strcpy(doc, ao, "type", arg_type_name(a->type)); - yyjson_mut_obj_add_bool(r->rdoc, ao, "required", a->required != 0); + yyjson_mut_obj_add_bool(doc, ao, "required", a->required != 0); if (a->def) - yyjson_mut_obj_add_strcpy(r->rdoc, ao, "default", a->def); + yyjson_mut_obj_add_strcpy(doc, ao, "default", a->def); if (a->values) { - yyjson_mut_val *vals = yyjson_mut_arr(r->rdoc); + yyjson_mut_val *vals = yyjson_mut_arr(doc); const char *p = a->values; while (*p) { const char *comma = strchr(p, ','); size_t len = comma ? (size_t)(comma - p) : strlen(p); - yyjson_mut_arr_add_strn(r->rdoc, vals, p, len); + yyjson_mut_arr_add_strn(doc, vals, p, len); if (!comma) break; p = comma + 1; } - yyjson_mut_obj_add_val(r->rdoc, ao, "values", vals); + yyjson_mut_obj_add_val(doc, ao, "values", vals); } if (a->desc) - yyjson_mut_obj_add_strcpy(r->rdoc, ao, "description", a->desc); + yyjson_mut_obj_add_strcpy(doc, ao, "description", a->desc); } - yyjson_mut_obj_add_val(r->rdoc, o, "args", args); + yyjson_mut_obj_add_val(doc, o, "args", args); return o; } +yyjson_mut_val *commands_describe(yyjson_mut_doc *doc) +{ + yyjson_mut_val *arr = yyjson_mut_arr(doc); + for (size_t t = 0; t < g_command_tables_count; t++) { + const struct cmd_table *tab = g_command_tables[t]; + for (size_t i = 0; i < tab->n; i++) + yyjson_mut_arr_add_val(arr, command_json(doc, &tab->cmds[i])); + } + return arr; +} + static yyjson_mut_val *h_describe(struct req *r) { const char *name = arg_str(r->args, "cmd"); @@ -66,16 +78,11 @@ static yyjson_mut_val *h_describe(struct req *r) const struct command *c = command_find(name); if (!c) return fail(r, "NOT_FOUND", "unknown command"); - yyjson_mut_obj_add_val(r->rdoc, o, "command", command_json(r, c)); + yyjson_mut_obj_add_val(r->rdoc, o, "command", + command_json(r->rdoc, c)); return o; } - yyjson_mut_val *arr = yyjson_mut_arr(r->rdoc); - for (size_t t = 0; t < g_command_tables_count; t++) { - const struct cmd_table *tab = g_command_tables[t]; - for (size_t i = 0; i < tab->n; i++) - yyjson_mut_arr_add_val(arr, command_json(r, &tab->cmds[i])); - } - yyjson_mut_obj_add_val(r->rdoc, o, "commands", arr); + yyjson_mut_obj_add_val(r->rdoc, o, "commands", commands_describe(r->rdoc)); return o; } diff --git a/src/commands.h b/src/commands.h index e7e71f6..3bcd447 100644 --- a/src/commands.h +++ b/src/commands.h @@ -50,6 +50,7 @@ extern const size_t g_command_tables_count; int command_validate_args(const struct command *cmd, yyjson_val *args, char *err, size_t errlen); const struct command *command_find(const char *name); +yyjson_mut_val *commands_describe(yyjson_mut_doc *doc); const char *perm_name(enum cmd_perm p); const char *perm_scope(enum cmd_perm p); -- cgit v1.3