diff options
Diffstat (limited to 'src/cmd_util.c')
| -rw-r--r-- | src/cmd_util.c | 412 |
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; +} |
