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 --- scripts/gen_protocol.c | 249 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 scripts/gen_protocol.c (limited to 'scripts/gen_protocol.c') 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; +} -- cgit v1.3