summaryrefslogtreecommitdiff
path: root/src/cmd_util.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd_util.c')
-rw-r--r--src/cmd_util.c412
1 files changed, 412 insertions, 0 deletions
diff --git a/src/cmd_util.c b/src/cmd_util.c
new file mode 100644
index 0000000..747f836
--- /dev/null
+++ b/src/cmd_util.c
@@ -0,0 +1,412 @@
+#include "cmd_util.h"
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "db.h"
+#include "util.h"
+
+/* ------------------------------------------------------------------ */
+/* small helpers */
+/* ------------------------------------------------------------------ */
+
+const char *sq(const unsigned char *p)
+{
+ return p ? (const char *)p : "";
+}
+
+const char *arg_str(yyjson_val *args, const char *key)
+{
+ if (!args || !yyjson_is_obj(args))
+ return NULL;
+ yyjson_val *v = yyjson_obj_get(args, key);
+ return v && yyjson_is_str(v) ? yyjson_get_str(v) : NULL;
+}
+
+int arg_int(yyjson_val *args, const char *key, int64_t *out)
+{
+ if (!args || !yyjson_is_obj(args))
+ return 0;
+ yyjson_val *v = yyjson_obj_get(args, key);
+ if (!v || !yyjson_is_int(v))
+ return 0;
+ *out = yyjson_get_int(v);
+ return 1;
+}
+
+int arg_bool(yyjson_val *args, const char *key, int *out)
+{
+ if (!args || !yyjson_is_obj(args))
+ return 0;
+ yyjson_val *v = yyjson_obj_get(args, key);
+ if (!v || !yyjson_is_bool(v))
+ return 0;
+ *out = yyjson_get_bool(v) ? 1 : 0;
+ return 1;
+}
+
+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";
+}
+
+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;
+}
+
+void arg_error(char *err, size_t errlen, const char *fmt, ...)
+ __attribute__((format(printf, 3, 4)));
+
+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;
+}
+
+yyjson_mut_val *fail(struct req *r, const char *code, const char *msg)
+{
+ r->err_code = code;
+ snprintf(r->err_msg, sizeof r->err_msg, "%s", msg);
+ return NULL;
+}
+
+yyjson_mut_val *failf(struct req *r, const char *code, const char *fmt, ...)
+ __attribute__((format(printf, 3, 4)));
+
+yyjson_mut_val *failf(struct req *r, const char *code, const char *fmt, ...)
+{
+ r->err_code = code;
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(r->err_msg, sizeof r->err_msg, fmt, ap);
+ va_end(ap);
+ return NULL;
+}
+
+int mkdir_p(const char *path, mode_t mode)
+{
+ char tmp[4096];
+ if (!path || strlen(path) >= sizeof tmp)
+ return -1;
+ strcpy(tmp, path);
+ for (char *p = tmp + 1; *p; p++) {
+ if (*p == '/') {
+ *p = '\0';
+ if (mkdir(tmp, mode) != 0 && errno != EEXIST)
+ return -1;
+ *p = '/';
+ }
+ }
+ if (mkdir(tmp, mode) != 0 && errno != EEXIST)
+ return -1;
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* ------------------------------------------------------------------ */
+/* shared helpers for M2 commands */
+/* ------------------------------------------------------------------ */
+
+yyjson_mut_val *json_to_mut(yyjson_mut_doc *doc, const char *json)
+{
+ if (!json)
+ return NULL;
+ yyjson_doc *d = yyjson_read(json, strlen(json), 0);
+ if (!d)
+ return NULL;
+ yyjson_mut_val *v = yyjson_val_mut_copy(doc, yyjson_doc_get_root(d));
+ yyjson_doc_free(d);
+ return v;
+}
+int is_digits(const char *s)
+{
+ if (!s || !*s)
+ return 0;
+ for (; *s; s++)
+ if (*s < '0' || *s > '9')
+ return 0;
+ return 1;
+}
+double parse_kr_double(const char *s, int *ok)
+{
+ *ok = 0;
+ if (!s)
+ return 0;
+ char buf[64];
+ size_t j = 0;
+ for (const char *p = s; *p && j < sizeof buf - 1; p++) {
+ if (*p == ' ' || *p == '\t')
+ continue;
+ buf[j++] = (*p == ',') ? '.' : *p;
+ }
+ buf[j] = '\0';
+ if (!j || (j == 1 && buf[0] == '-'))
+ return 0;
+ char *end = NULL;
+ double v = strtod(buf, &end);
+ if (!end || *end)
+ return 0;
+ *ok = 1;
+ return v;
+}
+
+char *replace_x(const char *text, double x)
+{
+ struct buf b;
+ buf_init(&b);
+ for (const char *p = text; *p;) {
+ if (p[0] == '{' && p[1] == 'x' && p[2] == '}') {
+ char tmp[64];
+ snprintf(tmp, sizeof tmp, "%.2f", x);
+ buf_append(&b, tmp, strlen(tmp));
+ p += 3;
+ } else {
+ buf_append(&b, p, 1);
+ p++;
+ }
+ }
+ buf_append(&b, "", 1);
+ return (char *)b.p;
+}
+
+int load_template(sqlite3 *db, int64_t org_id, int64_t id,
+ const char *name, struct tpl_head *head, char **err)
+{
+ sqlite3_stmt *st = NULL;
+ const char *sql =
+ id ? "SELECT id,name,series,description FROM voucher_templates"
+ " WHERE org_id=?1 AND id=?2 AND active=1"
+ : "SELECT id,name,series,description FROM voucher_templates"
+ " WHERE org_id=?1 AND name=?2 AND active=1";
+ if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
+ if (err)
+ *err = xstrdup("database error");
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ if (id)
+ sqlite3_bind_int64(st, 2, id);
+ else
+ sqlite3_bind_text(st, 2, name ? name : "", -1, SQLITE_TRANSIENT);
+ if (sqlite3_step(st) != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ if (err)
+ *err = xstrdup("template not found");
+ return -1;
+ }
+ head->id = sqlite3_column_int64(st, 0);
+ snprintf(head->name, sizeof head->name, "%s",
+ sqlite3_column_text(st, 1));
+ snprintf(head->series, sizeof head->series, "%s",
+ sqlite3_column_text(st, 2));
+ snprintf(head->description, sizeof head->description, "%s",
+ sqlite3_column_text(st, 3));
+ sqlite3_finalize(st);
+ return 0;
+}
+
+int load_template_rows(sqlite3 *db, int64_t org_id, int64_t tpl,
+ struct tpl_loaded **out, size_t *out_n,
+ char **err)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT a.number,tr.formula,COALESCE(tr.description,'')"
+ " FROM voucher_template_rows tr JOIN accounts a"
+ " ON a.org_id=tr.org_id AND a.id=tr.account_id"
+ " WHERE tr.org_id=?1 AND tr.template_id=?2 ORDER BY tr.line_no",
+ -1, &st, NULL) != SQLITE_OK) {
+ if (err)
+ *err = xstrdup("database error");
+ return -1;
+ }
+ sqlite3_bind_int64(st, 1, org_id);
+ sqlite3_bind_int64(st, 2, tpl);
+ size_t cap = 0, n = 0;
+ struct tpl_loaded *rows = NULL;
+ while (sqlite3_step(st) == SQLITE_ROW) {
+ if (n == cap) {
+ cap = cap ? cap * 2 : 8;
+ rows = xrealloc(rows, cap * sizeof *rows);
+ }
+ memset(&rows[n], 0, sizeof rows[n]);
+ snprintf(rows[n].account, sizeof rows[n].account, "%s",
+ sqlite3_column_text(st, 0));
+ snprintf(rows[n].formula, sizeof rows[n].formula, "%s",
+ sqlite3_column_text(st, 1));
+ snprintf(rows[n].desc, sizeof rows[n].desc, "%s",
+ sqlite3_column_text(st, 2));
+ n++;
+ }
+ sqlite3_finalize(st);
+ *out = rows;
+ *out_n = n;
+ return 0;
+}
+int64_t default_fy_id(sqlite3 *db, int64_t org_id)
+{
+ sqlite3_stmt *st = NULL;
+ int64_t id = 0;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT id FROM fiscal_years WHERE org_id=?1"
+ " ORDER BY start_date DESC LIMIT 1",
+ -1, &st, NULL) == SQLITE_OK) {
+ sqlite3_bind_int64(st, 1, org_id);
+ if (sqlite3_step(st) == SQLITE_ROW)
+ id = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ }
+ return id;
+}
+
+int req_fy(struct req *r, int64_t *out)
+{
+ int64_t fy = 0;
+ arg_int(r->args, "fiscal_year", &fy);
+ if (!fy)
+ fy = default_fy_id(r->db, r->org_id);
+ if (!fy) {
+ fail(r, "NOT_FOUND", "no fiscal year; open one first");
+ return -1;
+ }
+ *out = fy;
+ return 0;
+}
+
+unsigned char *read_file(const char *path, size_t *out_len)
+{
+ FILE *f = fopen(path, "rb");
+ if (!f)
+ return NULL;
+ struct buf b;
+ buf_init(&b);
+ unsigned char chunk[65536];
+ size_t rn;
+ while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0)
+ buf_append(&b, chunk, rn);
+ int bad = ferror(f);
+ fclose(f);
+ if (bad) {
+ buf_free(&b);
+ return NULL;
+ }
+ *out_len = b.len;
+ return b.p;
+}