summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 22:25:07 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 22:28:19 +0200
commit62504dda1527490cb49e56dd9840031479d258f0 (patch)
tree34d8ec924672bad497902d145e67999b6850a830
parent10e9e42bb6862a97539346c9d11c328fb73cfe2f (diff)
downloadbokf-62504dda1527490cb49e56dd9840031479d258f0.tar.gz
bokf-62504dda1527490cb49e56dd9840031479d258f0.zip
docs: generate the command catalogue from the command tables
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--Makefile18
-rw-r--r--docs/PROTOCOL.md99
-rwxr-xr-xscripts/check-consistency.sh7
-rw-r--r--scripts/gen_protocol.c249
-rw-r--r--src/commands.c69
-rw-r--r--src/commands.h1
6 files changed, 408 insertions, 35 deletions
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.
+<!-- generated:commands begin -->
+## 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` |
+<!-- generated:commands end -->
+
## 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 '
+ /<!-- generated:commands begin -->/ { skip = 1 }
+ /<!-- generated:commands end -->/ { 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "commands.h"
+#include "util.h"
+#include "yyjson.h"
+
+#define BEGIN_MARK "<!-- generated:commands begin -->"
+#define END_MARK "<!-- generated:commands end -->"
+#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);