summaryrefslogtreecommitdiff
path: root/scripts/gen_protocol.c
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/gen_protocol.c')
-rw-r--r--scripts/gen_protocol.c249
1 files changed, 249 insertions, 0 deletions
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;
+}