aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 09:35:06 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 09:35:41 +0200
commitcc0bf3467b33c32922ec78eb0cf4fffbd90483c2 (patch)
treeac52b8ca3d6680fefeac39b9fea55b7d49898069 /src
parentb6aeaa8995ae81562a4bf11937959b09027116ff (diff)
downloadbokf-cc0bf3467b33c32922ec78eb0cf4fffbd90483c2.tar.gz
bokf-cc0bf3467b33c32922ec78eb0cf4fffbd90483c2.zip
commands: declarative argument schemas with dispatch validation
Diffstat (limited to 'src')
-rw-r--r--src/commands.c669
-rw-r--r--src/commands.h18
-rw-r--r--src/protocol.c6
3 files changed, 625 insertions, 68 deletions
diff --git a/src/commands.c b/src/commands.c
index d21d4c8..d639183 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -63,6 +63,134 @@ static int arg_bool(yyjson_val *args, const char *key, int *out)
return 1;
}
+static const char *arg_type_name(enum arg_type type)
+{
+ switch (type) {
+ case ARG_STR:
+ return "string";
+ case ARG_INT:
+ return "int";
+ case ARG_BOOL:
+ return "bool";
+ case ARG_ENUM:
+ return "enum";
+ case ARG_DATE:
+ return "date";
+ case ARG_JSON:
+ return "json";
+ }
+ return "json";
+}
+
+static int enum_allowed(const char *values, const char *v)
+{
+ size_t n = strlen(v);
+ const char *p = values;
+ while (p && *p) {
+ const char *comma = strchr(p, ',');
+ size_t len = comma ? (size_t)(comma - p) : strlen(p);
+ if (len == n && strncmp(p, v, n) == 0)
+ return 1;
+ if (!comma)
+ break;
+ p = comma + 1;
+ }
+ return 0;
+}
+
+static void arg_error(char *err, size_t errlen, const char *fmt, ...)
+ __attribute__((format(printf, 3, 4)));
+
+static void arg_error(char *err, size_t errlen, const char *fmt, ...)
+{
+ if (!err || errlen == 0)
+ return;
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(err, errlen, fmt, ap);
+ va_end(ap);
+}
+
+int command_validate_args(const struct command *cmd, yyjson_val *args,
+ char *err, size_t errlen)
+{
+ if (!cmd || !cmd->args)
+ return 0;
+ if (err && errlen)
+ err[0] = '\0';
+ for (size_t i = 0; i < cmd->nargs; i++) {
+ const struct cmd_arg *a = &cmd->args[i];
+ yyjson_val *v = args && yyjson_is_obj(args)
+ ? yyjson_obj_get(args, a->name)
+ : NULL;
+ if (!v || yyjson_is_null(v)) {
+ if (a->required) {
+ arg_error(err, errlen, "missing required argument \"%s\"",
+ a->name);
+ return -1;
+ }
+ continue;
+ }
+ int bad = 0, empty = 0;
+ switch (a->type) {
+ case ARG_STR:
+ bad = !yyjson_is_str(v);
+ empty = !bad && a->required && !*yyjson_get_str(v);
+ break;
+ case ARG_INT:
+ bad = !yyjson_is_int(v);
+ break;
+ case ARG_BOOL:
+ bad = !yyjson_is_bool(v);
+ break;
+ case ARG_ENUM:
+ bad = !yyjson_is_str(v) ||
+ !enum_allowed(a->values, yyjson_get_str(v));
+ empty = !bad && a->required && !*yyjson_get_str(v);
+ break;
+ case ARG_DATE:
+ bad = !yyjson_is_str(v) ||
+ !util_parse_iso_date(yyjson_get_str(v));
+ break;
+ case ARG_JSON:
+ break;
+ }
+ if (empty) {
+ arg_error(err, errlen, "missing required argument \"%s\"",
+ a->name);
+ return -1;
+ }
+ if (bad) {
+ switch (a->type) {
+ case ARG_STR:
+ arg_error(err, errlen, "argument \"%s\" must be a string",
+ a->name);
+ break;
+ case ARG_INT:
+ arg_error(err, errlen, "argument \"%s\" must be an integer",
+ a->name);
+ break;
+ case ARG_BOOL:
+ arg_error(err, errlen, "argument \"%s\" must be a boolean",
+ a->name);
+ break;
+ case ARG_ENUM:
+ arg_error(err, errlen, "argument \"%s\" must be one of: %s",
+ a->name, a->values ? a->values : "");
+ break;
+ case ARG_DATE:
+ arg_error(err, errlen, "argument \"%s\" must be YYYY-MM-DD",
+ a->name);
+ break;
+ case ARG_JSON:
+ break;
+ }
+ return -1;
+ }
+ }
+ return 0;
+}
+
static yyjson_mut_val *fail(struct req *r, const char *code, const char *msg)
{
r->err_code = code;
@@ -1405,6 +1533,33 @@ static yyjson_mut_val *command_json(struct req *r, const struct command *c)
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);
+ 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",
+ arg_type_name(a->type));
+ yyjson_mut_obj_add_bool(r->rdoc, ao, "required", a->required != 0);
+ if (a->def)
+ yyjson_mut_obj_add_strcpy(r->rdoc, ao, "default", a->def);
+ if (a->values) {
+ yyjson_mut_val *vals = yyjson_mut_arr(r->rdoc);
+ 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);
+ if (!comma)
+ break;
+ p = comma + 1;
+ }
+ yyjson_mut_obj_add_val(r->rdoc, ao, "values", vals);
+ }
+ if (a->desc)
+ yyjson_mut_obj_add_strcpy(r->rdoc, ao, "description", a->desc);
+ }
+ yyjson_mut_obj_add_val(r->rdoc, o, "args", args);
return o;
}
@@ -4482,135 +4637,513 @@ static yyjson_mut_val *h_sie_import(struct req *r)
/* command table */
/* ------------------------------------------------------------------ */
+static const struct cmd_arg args_session_open[] = {
+ { "method", ARG_ENUM, 1, NULL, "password,token",
+ "Open with a password or an API token" },
+ { "username", ARG_STR, 0, NULL, NULL, "Username for method=password" },
+ { "password", ARG_STR, 0, NULL, NULL, "Password for method=password" },
+ { "token", ARG_STR, 0, NULL, NULL, "API token for method=token" },
+};
+
+static const struct cmd_arg args_session_use_org[] = {
+ { "org", ARG_INT, 1, NULL, NULL, "Org id" },
+};
+
+static const struct cmd_arg args_org_create[] = {
+ { "name", ARG_STR, 1, NULL, NULL, "Org name" },
+ { "org_nr", ARG_STR, 0, NULL, NULL, "Swedish org number" },
+ { "fiscal_year_start_month", ARG_INT, 0, "1", NULL,
+ "Fiscal year start month 1-12" },
+ { "moms_period", ARG_ENUM, 0, "month", "month,quarter,year",
+ "VAT reporting period" },
+ { "framework", ARG_ENUM, 0, "K2", "K2,K3", "Accounting framework" },
+};
+
+static const struct cmd_arg args_org_update[] = {
+ { "name", ARG_STR, 0, NULL, NULL, "Org name" },
+ { "org_nr", ARG_STR, 0, NULL, NULL, "Swedish org number" },
+ { "vat_nr", ARG_STR, 0, NULL, NULL, "VAT number" },
+ { "address", ARG_STR, 0, NULL, NULL, "Street address" },
+ { "postal_code", ARG_STR, 0, NULL, NULL, "Postal code" },
+ { "city", ARG_STR, 0, NULL, NULL, "City" },
+ { "country", ARG_STR, 0, NULL, NULL, "Country" },
+ { "email", ARG_STR, 0, NULL, NULL, "E-mail" },
+ { "phone", ARG_STR, 0, NULL, NULL, "Phone" },
+ { "moms_period", ARG_ENUM, 0, NULL, "month,quarter,year",
+ "VAT reporting period" },
+ { "framework", ARG_ENUM, 0, NULL, "K2,K3", "Accounting framework" },
+ { "description", ARG_STR, 0, NULL, NULL, "Free-text description" },
+ { "fiscal_year_start_month", ARG_INT, 0, NULL, NULL,
+ "Fiscal year start month 1-12" },
+ { "shares", ARG_INT, 0, NULL, NULL, "Number of shares" },
+};
+
+static const struct cmd_arg args_member_role[] = {
+ { "username", ARG_STR, 1, NULL, NULL, "Username" },
+ { "role", ARG_ENUM, 1, NULL, "owner,bookkeeper,viewer",
+ "Membership role" },
+};
+
+static const struct cmd_arg args_member_username[] = {
+ { "username", ARG_STR, 1, NULL, NULL, "Username" },
+};
+
+static const struct cmd_arg args_board_add[] = {
+ { "name", ARG_STR, 1, NULL, NULL, "Board member name" },
+ { "title", ARG_STR, 0, "Styrelseledamot", NULL, "Role on the board" },
+};
+
+static const struct cmd_arg args_board_update[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Board member id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Board member name" },
+ { "title", ARG_STR, 0, NULL, NULL, "Role on the board" },
+};
+
+static const struct cmd_arg args_board_remove[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Board member id" },
+};
+
+static const struct cmd_arg args_user_create[] = {
+ { "username", ARG_STR, 1, NULL, NULL, "Username" },
+ { "password", ARG_STR, 1, NULL, NULL, "Password" },
+ { "display_name", ARG_STR, 0, NULL, NULL, "Display name" },
+ { "is_admin", ARG_BOOL, 0, "false", NULL, "System admin" },
+};
+
+static const struct cmd_arg args_token_create[] = {
+ { "label", ARG_STR, 1, NULL, NULL, "Label shown in token.list" },
+ { "scopes", ARG_JSON, 0, NULL, NULL, "Array of read, write, admin" },
+ { "expires_at", ARG_DATE, 0, NULL, NULL, "Expiry date" },
+};
+
+static const struct cmd_arg args_token_revoke[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Token id" },
+};
+
+static const struct cmd_arg args_describe[] = {
+ { "cmd", ARG_STR, 0, NULL, NULL,
+ "Command name; omit for the full catalogue" },
+};
+
+static const struct cmd_arg args_audit_list[] = {
+ { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" },
+ { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" },
+ { "action", ARG_STR, 0, NULL, NULL, "Filter by action" },
+};
+
+static const struct cmd_arg args_audit_verify[] = {
+ { "full", ARG_BOOL, 0, "false", NULL, "Also re-hash attachments" },
+};
+
+static const struct cmd_arg args_backup_snapshot[] = {
+ { "dest", ARG_STR, 0, NULL, NULL,
+ "Destination path; defaults to backup_dir with a timestamp" },
+};
+
+static const struct cmd_arg args_account_list[] = {
+ { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active accounts" },
+};
+
+static const struct cmd_arg args_account_get[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Account id" },
+ { "number", ARG_STR, 0, NULL, NULL, "Account number (string)" },
+};
+
+static const struct cmd_arg args_account_create[] = {
+ { "number", ARG_STR, 1, NULL, NULL, "Account number, 1-10 digits" },
+ { "name", ARG_STR, 1, NULL, NULL, "Account name" },
+ { "type", ARG_ENUM, 1, NULL, "asset,liability,equity,revenue,expense",
+ "Account type" },
+ { "sru_code", ARG_STR, 0, NULL, NULL, "SRU code" },
+ { "vat_code", ARG_STR, 0, NULL, NULL, "VAT code" },
+};
+
+static const struct cmd_arg args_account_update[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Account id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Account name" },
+ { "sru_code", ARG_STR, 0, NULL, NULL, "SRU code" },
+ { "vat_code", ARG_STR, 0, NULL, NULL, "VAT code" },
+ { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" },
+};
+
+static const struct cmd_arg args_fiscal_year_get[] = {
+ { "id", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+};
+
+static const struct cmd_arg args_fiscal_year_open[] = {
+ { "label", ARG_STR, 1, NULL, NULL, "Fiscal year label" },
+ { "start_date", ARG_DATE, 1, NULL, NULL, "Start date (YYYY-MM-DD)" },
+ { "end_date", ARG_DATE, 1, NULL, NULL, "End date (YYYY-MM-DD)" },
+};
+
+static const struct cmd_arg args_fiscal_year_close[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "confirm", ARG_BOOL, 1, NULL, NULL, "Must be true" },
+};
+
+static const struct cmd_arg args_fiscal_year_reopen[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "confirm", ARG_BOOL, 1, NULL, NULL, "Must be true" },
+};
+
+static const struct cmd_arg args_fiscal_year_update[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "dividend_ore", ARG_INT, 0, NULL, NULL, "Proposed dividend in öre" },
+ { "events", ARG_STR, 0, NULL, NULL, "Material events during the year" },
+ { "agm_date", ARG_STR, 0, NULL, NULL, "AGM date; empty clears" },
+ { "dividend_date", ARG_STR, 0, NULL, NULL,
+ "Dividend payment date; empty clears" },
+ { "employees", ARG_STR, 0, NULL, NULL, "Average number of employees" },
+ { "notes", ARG_STR, 0, NULL, NULL, "Other notes" },
+};
+
+static const struct cmd_arg args_period_lock[] = {
+ { "fiscal_year", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "until", ARG_DATE, 1, NULL, NULL, "Lock through this date, inclusive" },
+ { "reason", ARG_STR, 0, NULL, NULL, "Reason, recorded in the audit log" },
+};
+
+static const struct cmd_arg args_period_unlock[] = {
+ { "fiscal_year", ARG_INT, 1, NULL, NULL, "Fiscal year id" },
+ { "reason", ARG_STR, 0, NULL, NULL, "Reason, recorded in the audit log" },
+};
+
+static const struct cmd_arg args_voucher_post[] = {
+ { "date", ARG_DATE, 1, NULL, NULL, "Voucher date (YYYY-MM-DD)" },
+ { "description", ARG_STR, 0, NULL, NULL, "Voucher text" },
+ { "series", ARG_STR, 0, NULL, NULL,
+ "Number series; defaults to settings.default_series" },
+ { "client_ref", ARG_STR, 0, NULL, NULL,
+ "Idempotency key, unique per org" },
+ { "corrects_voucher", ARG_INT, 0, NULL, NULL,
+ "Voucher id this one corrects" },
+ { "rows", ARG_JSON, 0, NULL, NULL,
+ "Array of {account,debit_ore,credit_ore,description?}" },
+ { "template", ARG_JSON, 0, NULL, NULL,
+ "Template id or name; alternative to rows" },
+ { "x", ARG_JSON, 0, NULL, NULL, "Template variable in kronor" },
+ { "attachment_ids", ARG_JSON, 0, NULL, NULL,
+ "Array of attachment ids to link" },
+};
+
+static const struct cmd_arg args_voucher_get[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Voucher id" },
+};
+
+static const struct cmd_arg args_voucher_list[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "from", ARG_DATE, 0, NULL, NULL, "Earliest date" },
+ { "to", ARG_DATE, 0, NULL, NULL, "Latest date" },
+ { "series", ARG_STR, 0, NULL, NULL, "Number series filter" },
+ { "account", ARG_STR, 0, NULL, NULL, "Account number filter" },
+ { "text", ARG_STR, 0, NULL, NULL, "Description substring filter" },
+ { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" },
+ { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" },
+};
+
+static const struct cmd_arg args_voucher_correct[] = {
+ { "voucher", ARG_INT, 1, NULL, NULL, "Voucher id to correct" },
+ { "description", ARG_STR, 1, NULL, NULL, "Correction text" },
+ { "date", ARG_DATE, 0, NULL, NULL, "Defaults to the original date" },
+ { "client_ref", ARG_STR, 0, NULL, NULL, "Idempotency key" },
+};
+
+static const struct cmd_arg args_bokslut_post[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "entries", ARG_JSON, 0, NULL, NULL,
+ "Array of {debit_account,credit_account,amount_ore,description?}" },
+ { "periodiseringsfond_ore", ARG_INT, 0, NULL, NULL,
+ "Periodiseringsfond in öre" },
+ { "tax_rate", ARG_JSON, 0, "20.6", NULL, "Tax rate percent, 0-100" },
+ { "dispose", ARG_BOOL, 0, "true", NULL,
+ "Post the result transfer to 8999/2099" },
+ { "date", ARG_DATE, 0, NULL, NULL,
+ "Posting date; defaults to the fiscal year end" },
+};
+
+static const struct cmd_arg args_settings_set[] = {
+ { "key", ARG_STR, 1, NULL, NULL, "default_series or attachment_dir" },
+ { "value", ARG_STR, 1, NULL, NULL, "Setting value" },
+};
+
+static const struct cmd_arg args_template_list[] = {
+ { "active_only", ARG_BOOL, 0, NULL, NULL, "Only active templates" },
+};
+
+static const struct cmd_arg args_template_get[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Template id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Template name" },
+};
+
+static const struct cmd_arg args_template_create[] = {
+ { "name", ARG_STR, 1, NULL, NULL, "Template name" },
+ { "series", ARG_STR, 0, NULL, NULL,
+ "Number series; defaults to settings.default_series" },
+ { "description", ARG_STR, 0, NULL, NULL,
+ "Voucher text; {x} is replaced with the amount" },
+ { "rows", ARG_JSON, 1, NULL, NULL,
+ "Array of {account,formula,description?}" },
+};
+
+static const struct cmd_arg args_template_update[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Template id" },
+ { "name", ARG_STR, 0, NULL, NULL,
+ "Template name, or the new name when id is given" },
+ { "series", ARG_STR, 0, NULL, NULL, "Number series" },
+ { "description", ARG_STR, 0, NULL, NULL, "Voucher text" },
+ { "active", ARG_BOOL, 0, NULL, NULL, "Active flag" },
+ { "rows", ARG_JSON, 0, NULL, NULL,
+ "Replacement rows, {account,formula,description?}" },
+};
+
+static const struct cmd_arg args_template_archive[] = {
+ { "id", ARG_INT, 0, NULL, NULL, "Template id" },
+ { "name", ARG_STR, 0, NULL, NULL, "Template name" },
+};
+
+static const struct cmd_arg args_attachment_put[] = {
+ { "filename", ARG_STR, 1, NULL, NULL, "File name" },
+ { "mime", ARG_STR, 0, "application/octet-stream", NULL, "MIME type" },
+ { "content_base64", ARG_STR, 1, NULL, NULL, "File content, base64" },
+ { "voucher_id", ARG_INT, 0, NULL, NULL, "Link to this voucher" },
+};
+
+static const struct cmd_arg args_attachment_link[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Attachment id" },
+ { "voucher_id", ARG_INT, 1, NULL, NULL, "Voucher id" },
+};
+
+static const struct cmd_arg args_attachment_unlink[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Attachment id" },
+ { "voucher_id", ARG_INT, 1, NULL, NULL, "Voucher id" },
+};
+
+static const struct cmd_arg args_attachment_get[] = {
+ { "id", ARG_INT, 1, NULL, NULL, "Attachment id" },
+};
+
+static const struct cmd_arg args_attachment_list[] = {
+ { "voucher_id", ARG_INT, 0, NULL, NULL,
+ "Only attachments on this voucher" },
+ { "unlinked", ARG_BOOL, 0, NULL, NULL,
+ "Only unlinked attachments (inbox)" },
+ { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" },
+ { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" },
+};
+
+static const struct cmd_arg args_report_trial_balance[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "from", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
+ { "to", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
+ { "include_zero", ARG_BOOL, 0, "false", NULL,
+ "Include accounts with no activity" },
+};
+
+static const struct cmd_arg args_report_income_statement[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "from", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
+ { "to", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
+};
+
+static const struct cmd_arg args_report_balance_sheet[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "to", ARG_DATE, 0, NULL, NULL, "Report through this date" },
+};
+
+static const struct cmd_arg args_report_vat[] = {
+ { "from", ARG_DATE, 1, NULL, NULL, "Period start" },
+ { "to", ARG_DATE, 1, NULL, NULL, "Period end" },
+};
+
+static const struct cmd_arg args_report_general_ledger[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "accounts", ARG_JSON, 0, NULL, NULL,
+ "Array of account numbers to include" },
+ { "from", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
+ { "to", ARG_DATE, 0, NULL, NULL, "Narrow the period" },
+};
+
+static const struct cmd_arg args_report_voucher_list[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "series", ARG_STR, 0, NULL, NULL, "Number series filter" },
+};
+
+static const struct cmd_arg args_report_vat_eskd[] = {
+ { "from", ARG_DATE, 1, NULL, NULL, "Period start" },
+ { "to", ARG_DATE, 1, NULL, NULL, "Period end" },
+ { "upplysning", ARG_STR, 0, NULL, NULL,
+ "Free-text message to Skatteverket" },
+};
+
+static const struct cmd_arg args_sru_export[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "adjustments", ARG_JSON, 0, NULL, NULL,
+ "Array of {code,amount_ore} tax adjustments" },
+ { "submitter", ARG_JSON, 0, NULL, NULL,
+ "Object overriding the INFO.SRU submitter" },
+ { "assisted", ARG_BOOL, 0, NULL, NULL, "Assisted declaration" },
+ { "audited", ARG_BOOL, 0, NULL, NULL, "Audited declaration" },
+ { "ignore_unmapped", ARG_BOOL, 0, "false", NULL,
+ "Proceed despite unmapped accounts" },
+};
+
+static const struct cmd_arg args_sie_export[] = {
+ { "fiscal_year", ARG_INT, 0, NULL, NULL,
+ "Fiscal year id; defaults to the latest" },
+ { "inline", ARG_BOOL, 0, "false", NULL, "Also return content_base64" },
+};
+
+static const struct cmd_arg args_sie_import[] = {
+ { "content_base64", ARG_STR, 0, NULL, NULL, "SIE file content, base64" },
+ { "path", ARG_STR, 0, NULL, NULL,
+ "Server-side path; alternative to content_base64" },
+};
+
const struct command g_commands[] = {
- { "health", "Liveness probe", PERM_PUBLIC, 0, 0, 0, h_health },
- { "meta", "Server metadata and limits", PERM_PUBLIC, 0, 0, 0, h_meta },
+ { "health", "Liveness probe", PERM_PUBLIC, 0, 0, 0, h_health, NULL, 0 },
+ { "meta", "Server metadata and limits", PERM_PUBLIC, 0, 0, 0, h_meta, NULL,
+ 0 },
{ "session.open", "Open a session (password or token)", PERM_PUBLIC, 0, 0,
- 0, h_session_open },
+ 0, h_session_open, CMD_ARGS(args_session_open) },
{ "session.close", "Close the current session", PERM_READ, 0, 0, 0,
- h_session_close },
+ h_session_close, NULL, 0 },
{ "session.whoami", "Current user, org, role and scopes", PERM_READ, 0, 0,
- 0, h_session_whoami },
+ 0, h_session_whoami, NULL, 0 },
{ "session.list_orgs", "Orgs the current user is a member of", PERM_READ,
- 0, 0, 0, h_session_list_orgs },
+ 0, 0, 0, h_session_list_orgs, NULL, 0 },
{ "session.use_org", "Switch active org", PERM_READ, 0, 0, 0,
- h_session_use_org },
+ h_session_use_org, CMD_ARGS(args_session_use_org) },
{ "org.create", "Create an org and become its owner", PERM_READ, 0, 1, 1,
- h_org_create },
+ h_org_create, CMD_ARGS(args_org_create) },
{ "org.list", "List orgs for the current user", PERM_READ, 0, 0, 0,
- h_org_list },
- { "org.get", "Get org details", PERM_READ, 1, 0, 0, h_org_get },
+ h_org_list, NULL, 0 },
+ { "org.get", "Get org details", PERM_READ, 1, 0, 0, h_org_get, NULL, 0 },
{ "org.update", "Update org details (owner)", PERM_OWNER, 1, 1, 1,
- h_org_update },
+ h_org_update, CMD_ARGS(args_org_update) },
{ "org.member_list", "List org members", PERM_READ, 1, 0, 0,
- h_org_member_list },
+ h_org_member_list, NULL, 0 },
{ "org.member_add", "Add a member to the org", PERM_OWNER, 1, 1, 1,
- h_org_member_add },
+ h_org_member_add, CMD_ARGS(args_member_role) },
{ "org.member_set_role", "Change a member's role", PERM_OWNER, 1, 1, 1,
- h_org_member_set_role },
+ h_org_member_set_role, CMD_ARGS(args_member_role) },
{ "org.member_remove", "Remove a member from the org", PERM_OWNER, 1, 1, 1,
- h_org_member_remove },
- { "board.list", "List board members", PERM_READ, 1, 0, 0, h_board_list },
- { "board.add", "Add a board member", PERM_OWNER, 1, 1, 1, h_board_add },
+ h_org_member_remove, CMD_ARGS(args_member_username) },
+ { "board.list", "List board members", PERM_READ, 1, 0, 0, h_board_list,
+ NULL, 0 },
+ { "board.add", "Add a board member", PERM_OWNER, 1, 1, 1, h_board_add,
+ CMD_ARGS(args_board_add) },
{ "board.update", "Update a board member", PERM_OWNER, 1, 1, 1,
- h_board_update },
+ h_board_update, CMD_ARGS(args_board_update) },
{ "board.remove", "Remove a board member", PERM_OWNER, 1, 1, 1,
- h_board_remove },
+ h_board_remove, CMD_ARGS(args_board_remove) },
{ "user.create", "Create a user (system admin)", PERM_ADMIN, 0, 1, 1,
- h_user_create },
+ h_user_create, CMD_ARGS(args_user_create) },
{ "user.list", "List users (system admin)", PERM_ADMIN, 0, 0, 0,
- h_user_list },
+ h_user_list, NULL, 0 },
{ "token.create", "Create an API token for the active org", PERM_READ, 1,
- 1, 1, h_token_create },
+ 1, 1, h_token_create, CMD_ARGS(args_token_create) },
{ "token.list", "List your API tokens in the active org", PERM_READ, 1, 0,
- 0, h_token_list },
+ 0, h_token_list, NULL, 0 },
{ "token.revoke", "Revoke an API token", PERM_READ, 1, 1, 1,
- h_token_revoke },
+ h_token_revoke, CMD_ARGS(args_token_revoke) },
{ "describe", "List implemented commands and permissions", PERM_READ, 0, 0,
- 0, h_describe },
+ 0, h_describe, CMD_ARGS(args_describe) },
{ "agent.instructions", "Workflow rules for agents", PERM_READ, 0, 0, 0,
- h_agent_instructions },
+ h_agent_instructions, NULL, 0 },
{ "audit.list", "Read the audit log for the active org", PERM_READ, 1, 0,
- 0, h_audit_list },
+ 0, h_audit_list, CMD_ARGS(args_audit_list) },
{ "audit.verify", "Verify the voucher and audit hash chains", PERM_READ, 0,
- 0, 0, h_audit_verify },
+ 0, 0, h_audit_verify, CMD_ARGS(args_audit_verify) },
{ "backup.snapshot", "Create a consistent database snapshot", PERM_ADMIN,
- 0, 1, 0, h_backup_snapshot },
+ 0, 1, 0, h_backup_snapshot, CMD_ARGS(args_backup_snapshot) },
{ "account.list", "List the chart of accounts", PERM_READ, 1, 0, 0,
- h_account_list },
- { "account.get", "Get one account", PERM_READ, 1, 0, 0, h_account_get },
+ h_account_list, CMD_ARGS(args_account_list) },
+ { "account.get", "Get one account", PERM_READ, 1, 0, 0, h_account_get,
+ CMD_ARGS(args_account_get) },
{ "account.create", "Add an account", PERM_WRITE, 1, 1, 1,
- h_account_create },
+ h_account_create, CMD_ARGS(args_account_create) },
{ "account.update", "Rename or deactivate an account", PERM_WRITE, 1, 1, 1,
- h_account_update },
+ h_account_update, CMD_ARGS(args_account_update) },
{ "fiscal_year.list", "List fiscal years", PERM_READ, 1, 0, 0,
- h_fiscal_year_list },
+ h_fiscal_year_list, NULL, 0 },
{ "fiscal_year.get", "Get a fiscal year", PERM_READ, 1, 0, 0,
- h_fiscal_year_get },
+ h_fiscal_year_get, CMD_ARGS(args_fiscal_year_get) },
{ "fiscal_year.open", "Open a new fiscal year", PERM_OWNER, 1, 1, 1,
- h_fiscal_year_open },
+ h_fiscal_year_open, CMD_ARGS(args_fiscal_year_open) },
{ "fiscal_year.close", "Close a fiscal year", PERM_OWNER, 1, 1, 0,
- h_fiscal_year_close },
+ h_fiscal_year_close, CMD_ARGS(args_fiscal_year_close) },
{ "fiscal_year.reopen", "Reopen a closed fiscal year", PERM_OWNER, 1, 1, 1,
- h_fiscal_year_reopen },
+ h_fiscal_year_reopen, CMD_ARGS(args_fiscal_year_reopen) },
{ "fiscal_year.update", "Update fiscal-year metadata (dividend)",
- PERM_WRITE, 1, 1, 1, h_fiscal_year_update },
+ PERM_WRITE, 1, 1, 1, h_fiscal_year_update,
+ CMD_ARGS(args_fiscal_year_update) },
{ "period.lock", "Lock a period through a date", PERM_OWNER, 1, 1, 1,
- h_period_lock },
+ h_period_lock, CMD_ARGS(args_period_lock) },
{ "period.unlock", "Remove a period lock", PERM_OWNER, 1, 1, 1,
- h_period_unlock },
+ h_period_unlock, CMD_ARGS(args_period_unlock) },
{ "voucher.post", "Post an immutable voucher (rows or template+x)",
- PERM_WRITE, 1, 1, 1, h_voucher_post },
+ PERM_WRITE, 1, 1, 1, h_voucher_post, CMD_ARGS(args_voucher_post) },
{ "voucher.get", "Get a voucher with rows", PERM_READ, 1, 0, 0,
- h_voucher_get },
- { "voucher.list", "List vouchers", PERM_READ, 1, 0, 0, h_voucher_list },
+ h_voucher_get, CMD_ARGS(args_voucher_get) },
+ { "voucher.list", "List vouchers", PERM_READ, 1, 0, 0, h_voucher_list,
+ CMD_ARGS(args_voucher_list) },
{ "voucher.correct", "Post an ändringsverifikat", PERM_WRITE, 1, 1, 1,
- h_voucher_correct },
+ h_voucher_correct, CMD_ARGS(args_voucher_correct) },
{ "bokslut.post", "Year-end: dispositions, tax and result transfer",
- PERM_WRITE, 1, 1, 1, h_bokslut_post },
+ PERM_WRITE, 1, 1, 1, h_bokslut_post, CMD_ARGS(args_bokslut_post) },
{ "settings.get", "Read org settings", PERM_READ, 1, 0, 0,
- h_settings_get },
+ h_settings_get, NULL, 0 },
{ "settings.set", "Change an org setting", PERM_WRITE, 1, 1, 1,
- h_settings_set },
+ h_settings_set, CMD_ARGS(args_settings_set) },
{ "template.list", "List voucher templates", PERM_READ, 1, 0, 0,
- h_template_list },
+ h_template_list, CMD_ARGS(args_template_list) },
{ "template.get", "Get a voucher template with its rows", PERM_READ, 1, 0,
- 0, h_template_get },
+ 0, h_template_get, CMD_ARGS(args_template_get) },
{ "template.create", "Create a voucher template", PERM_WRITE, 1, 1, 1,
- h_template_create },
+ h_template_create, CMD_ARGS(args_template_create) },
{ "template.update", "Update a voucher template", PERM_WRITE, 1, 1, 1,
- h_template_update },
+ h_template_update, CMD_ARGS(args_template_update) },
{ "template.archive", "Archive a voucher template", PERM_WRITE, 1, 1, 1,
- h_template_archive },
+ h_template_archive, CMD_ARGS(args_template_archive) },
{ "attachment.put", "Store an underlag (receipt, invoice)", PERM_WRITE, 1,
- 1, 1, h_attachment_put },
+ 1, 1, h_attachment_put, CMD_ARGS(args_attachment_put) },
{ "attachment.link", "Link an existing attachment to a voucher",
- PERM_WRITE, 1, 1, 1, h_attachment_link },
+ PERM_WRITE, 1, 1, 1, h_attachment_link, CMD_ARGS(args_attachment_link) },
{ "attachment.unlink", "Remove an attachment link", PERM_WRITE, 1, 1, 1,
- h_attachment_unlink },
+ h_attachment_unlink, CMD_ARGS(args_attachment_unlink) },
{ "attachment.get", "Fetch an attachment", PERM_READ, 1, 0, 0,
- h_attachment_get },
+ h_attachment_get, CMD_ARGS(args_attachment_get) },
{ "attachment.list", "List attachments / inbox", PERM_READ, 1, 0, 0,
- h_attachment_list },
+ h_attachment_list, CMD_ARGS(args_attachment_list) },
{ "report.trial_balance", "Saldobalans (IB, period, UB)", PERM_READ, 1, 0,
- 0, h_report_trial_balance },
+ 0, h_report_trial_balance, CMD_ARGS(args_report_trial_balance) },
{ "report.income_statement", "Resultaträkning", PERM_READ, 1, 0, 0,
- h_report_income_statement },
+ h_report_income_statement, CMD_ARGS(args_report_income_statement) },
{ "report.balance_sheet", "Balansräkning", PERM_READ, 1, 0, 0,
- h_report_balance_sheet },
+ h_report_balance_sheet, CMD_ARGS(args_report_balance_sheet) },
{ "report.vat", "Momsdeklaration ruta för ruta", PERM_READ, 1, 0, 0,
- h_report_vat },
+ h_report_vat, CMD_ARGS(args_report_vat) },
{ "report.general_ledger", "Huvudbok", PERM_READ, 1, 0, 0,
- h_report_general_ledger },
+ h_report_general_ledger, CMD_ARGS(args_report_general_ledger) },
{ "report.voucher_list", "Verifikationslista", PERM_READ, 1, 0, 0,
- h_report_voucher_list },
+ h_report_voucher_list, CMD_ARGS(args_report_voucher_list) },
{ "report.vat_eskd", "eSKD XML for the momsdeklaration (ISO-8859-1)",
- PERM_READ, 1, 0, 0, h_report_vat_eskd },
+ PERM_READ, 1, 0, 0, h_report_vat_eskd, CMD_ARGS(args_report_vat_eskd) },
{ "sru.export", "INK2 SRU files (INFO.SRU + BLANKETTER.SRU)",
- PERM_READ, 1, 0, 0, h_sru_export },
- { "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export },
+ PERM_READ, 1, 0, 0, h_sru_export, CMD_ARGS(args_sru_export) },
+ { "sie.export", "Export SIE 4 (PC8)", PERM_READ, 1, 0, 0, h_sie_export,
+ CMD_ARGS(args_sie_export) },
{ "sie.import", "Import SIE 4 into an empty fiscal year", PERM_WRITE, 1, 1,
- 1, h_sie_import },
+ 1, h_sie_import, CMD_ARGS(args_sie_import) },
};
const size_t g_commands_count = sizeof g_commands / sizeof g_commands[0];
diff --git a/src/commands.h b/src/commands.h
index d04affc..e3168a5 100644
--- a/src/commands.h
+++ b/src/commands.h
@@ -4,6 +4,7 @@
#include <stddef.h>
#include "protocol.h"
+#include "yyjson.h"
enum cmd_perm {
PERM_PUBLIC = 0,
@@ -13,6 +14,17 @@ enum cmd_perm {
PERM_ADMIN
};
+enum arg_type { ARG_STR, ARG_INT, ARG_BOOL, ARG_ENUM, ARG_DATE, ARG_JSON };
+
+struct cmd_arg {
+ const char *name;
+ enum arg_type type;
+ int required; /* 1 = must be present; ARG_STR/ARG_ENUM must be non-empty */
+ const char *def; /* default as text, or NULL */
+ const char *values;/* ARG_ENUM: comma-separated allowed values, else NULL */
+ const char *desc; /* one-line description for describe, may be NULL */
+};
+
struct command {
const char *name;
const char *summary;
@@ -21,11 +33,17 @@ struct command {
int mutating;
int dry_run;
yyjson_mut_val *(*fn)(struct req *r);
+ const struct cmd_arg *args;
+ size_t nargs;
};
+#define CMD_ARGS(a) (a), (sizeof (a) / sizeof (a)[0])
+
extern const struct command g_commands[];
extern const size_t g_commands_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);
const char *perm_name(enum cmd_perm p);
const char *perm_scope(enum cmd_perm p);
diff --git a/src/protocol.c b/src/protocol.c
index c528759..d9019ba 100644
--- a/src/protocol.c
+++ b/src/protocol.c
@@ -176,6 +176,12 @@ char *protocol_handle_line(sqlite3 *db, const char *line, size_t len)
}
{
+ char verr[512];
+ if (cmd->nargs &&
+ command_validate_args(cmd, r.args, verr, sizeof verr) != 0) {
+ add_error(rdoc, resp, "INVALID_ARGS", verr, NULL);
+ goto done;
+ }
yyjson_mut_val *result = cmd->fn(&r);
if (!result) {
add_error(rdoc, resp, r.err_code ? r.err_code : "INTERNAL",