/* 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; }