diff --git a/CMakeLists.txt b/CMakeLists.txt index ea40029..533a34e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,7 @@ pkg_check_modules(ALSA REQUIRED alsa) # Module/als add_executable(f00bar bar.c bar.h config.c config.h + config-verify.c config-verify.h decoration.h font.c font.h log.c log.h diff --git a/config-verify.c b/config-verify.c new file mode 100644 index 0000000..15e0a3a --- /dev/null +++ b/config-verify.c @@ -0,0 +1,762 @@ +#include "config-verify.h" + +#include +#include + +#define LOG_MODULE "config:verify" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "tllist.h" + +typedef tll(const char *) keychain_t; + +static keychain_t * +chain_push(keychain_t *chain, const char *key) +{ + tll_push_back(*chain, key); + return chain; +} + +static void +chain_pop(keychain_t *chain) +{ + tll_pop_back(*chain); +} + +static const char * +err_prefix(const keychain_t *chain, const struct yml_node *node) +{ + static char msg[4096]; + int idx = 0; + + tll_foreach(*chain, key) + idx += snprintf(&msg[idx], sizeof(msg) - idx, "%s.", key->item); + + /* Remove trailing "." */ + msg[idx - 1] = '\0'; + return msg; +} + +static bool +verify_string(keychain_t *chain, const struct yml_node *node) +{ + const char *s = yml_value_as_string(node); + if (s == NULL) { + LOG_ERR("%s: value must be a string", err_prefix(chain, node)); + return false; + } + + return true; +} + +static bool +verify_int(keychain_t *chain, const struct yml_node *node) +{ + if (yml_value_is_int(node)) + return true; + + LOG_ERR("%s: value is not an integer: '%s'", + err_prefix(chain, node), yml_value_as_string(node)); + return false; +} + +static bool +verify_enum(keychain_t *chain, const struct yml_node *node, + const char *values[], size_t count) +{ + const char *s = yml_value_as_string(node); + if (s == NULL) { + LOG_ERR("%s: value must be a string", err_prefix(chain, node)); + return false; + } + + for (size_t i = 0; s != NULL && i < count; i++) { + if (strcmp(s, values[i]) == 0) + return true; + } + + LOG_ERR("%s: value must be one of:", err_prefix(chain, node)); + for (size_t i = 0; i < count; i++) + LOG_ERR(" %s", values[i]); + + return false; +} + +static bool +verify_color(keychain_t *chain, const struct yml_node *node) +{ + const char *s = yml_value_as_string(node); + if (s == NULL) { + LOG_ERR("%s: value must be a string", err_prefix(chain, node)); + return false; + } + + unsigned int r, g, b, a; + int v = sscanf(s, "%02x%02x%02x%02x", &r, &g, &b, &a); + + if (strlen(s) != 8 || v != 4) { + LOG_ERR("%s: value must be a color ('rrggbbaa', e.g ff00ffff)", + err_prefix(chain, node)); + return false; + } + + return true; +} + +static bool +verify_font(keychain_t *chain, const struct yml_node *node) +{ + if (!yml_is_dict(node)) { + LOG_ERR("%s: must be a dictionary", err_prefix(chain, node)); + return false; + } + + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *sub_key = yml_value_as_string(it.key); + if (sub_key == NULL) { + LOG_ERR("%s: font: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(sub_key, "size") == 0 || + strcmp(sub_key, "y_offset") == 0) + { + if (!verify_int(chain_push(chain, sub_key), it.value)) + return false; + } else if (strcmp(sub_key, "family") == 0) { + if (!verify_string(chain_push(chain, sub_key), it.value)) + return false; + } else { + LOG_ERR("%s: font: invalid key: %s", err_prefix(chain, node), sub_key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_border(keychain_t *chain, const struct yml_node *node) +{ + if (!yml_is_dict(node)) { + LOG_ERR("bar: border: must be a dictionary"); + return false; + } + + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "width") == 0) { + if (!verify_int(chain_push(chain, key), it.value)) + return false; + } else if (strcmp(key, "color") == 0) { + if (!verify_color(chain_push(chain, key), it.value)) + return false; + } else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_particle(keychain_t *chain, const struct yml_node *node) +{ + return true; +} + +static bool +verify_module_alsa(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "card") == 0 || + strcmp(key, "mixer") == 0) + { + if (!verify_string(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_backlight(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "name") == 0) { + if (!verify_string(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_battery(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "name") == 0) { + if (!verify_string(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "poll_interval") == 0) { + if (!verify_int(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_clock(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_i3(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "spacing") == 0 || + strcmp(key, "left_spacing") == 0 || + strcmp(key, "right_spacing") == 0) + { + if (!verify_int(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_label(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_mpd(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "host") == 0) { + if (!verify_string(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "port") == 0) { + if (!verify_int(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_network(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "name") == 0) { + if (!verify_string(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_removables(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "spacing") == 0 || + strcmp(key, "left_spacing") == 0 || + strcmp(key, "right_spacing") == 0) + { + if (!verify_int(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_xkb(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module_xwindow(keychain_t *chain, const struct yml_node *node) +{ + for (struct yml_dict_iter it = yml_dict_iter(node); + it.key != NULL; + yml_dict_next(&it)) + { + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("%s: key must be a string", err_prefix(chain, node)); + return false; + } + + if (strcmp(key, "content") == 0) { + if (!verify_particle(chain_push(chain, key), it.value)) + return false; + } + + else if (strcmp(key, "anchors") == 0) { + /* Skip */ + chain_push(chain, key); + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(chain, node), key); + return false; + } + + chain_pop(chain); + } + + return true; +} + +static bool +verify_module(keychain_t *chain, const struct yml_node *node) +{ + if (!yml_is_dict(node) || yml_dict_length(node) != 1) { + LOG_ERR("%s: module must be a dictionary with a single key; " + "the name of the module", err_prefix(chain, node)); + return false; + } + + struct yml_dict_iter m = yml_dict_iter(node); + const struct yml_node *module = m.key; + const struct yml_node *values = m.value; + + const char *mod_name = yml_value_as_string(module); + if (mod_name == NULL) { + LOG_ERR("%s: module name must be a string", err_prefix(chain, node)); + return false; + } + + static const struct { + const char *name; + bool (*verify_fun)(keychain_t *chain, const struct yml_node *node); + } modules[] = { + {"alsa", &verify_module_alsa}, + {"backlight", &verify_module_backlight}, + {"battery", &verify_module_battery}, + {"clock", &verify_module_clock}, + {"i3", &verify_module_i3}, + {"label", &verify_module_label}, + {"mpd", &verify_module_mpd}, + {"network", &verify_module_network}, + {"removables", &verify_module_removables}, + {"xkb", &verify_module_xkb}, + {"xwindow", &verify_module_xwindow}, + }; + + for (size_t i = 0; i < sizeof(modules) / sizeof(modules[0]); i++) { + if (strcmp(mod_name, modules[i].name) == 0) { + if (!modules[i].verify_fun(chain_push(chain, mod_name), values)) + return false; + chain_pop(chain); + return true; + } + } + + LOG_ERR("%s: invalid module name: %s", err_prefix(chain, node), mod_name); + return false; +} + +static bool +verify_module_list(keychain_t *chain, const struct yml_node *node) +{ + if (!yml_is_list(node)) { + LOG_ERR("%s: must be a list of modules", err_prefix(chain, node)); + return false; + } + + for (struct yml_list_iter it = yml_list_iter(node); + it.node != NULL; + yml_list_next(&it)) + { + if (!verify_module(chain, it.node)) + return false; + } + + return true; +} + +bool +config_verify_bar(const struct yml_node *bar) +{ + if (!yml_is_dict(bar)) { + LOG_ERR("bar is not a dictionary"); + return false; + } + + keychain_t chain = tll_init(); + chain_push(&chain, "bar"); + + bool ret = false; + + for (struct yml_dict_iter it = yml_dict_iter(bar); + it.key != NULL; + yml_dict_next(&it)) + { + if (!yml_is_scalar(it.key)) { + LOG_ERR("bar: key is not a scalar"); + goto err; + } + + const char *key = yml_value_as_string(it.key); + if (key == NULL) { + LOG_ERR("bar: key must be a string"); + goto err; + } + + if (strcmp(key, "height") == 0 || + strcmp(key, "spacing") == 0 || + strcmp(key, "left_spacing") == 0 || + strcmp(key, "right_spacing") == 0 || + strcmp(key, "margin") == 0 || + strcmp(key, "left_margin") == 0 || + strcmp(key, "right_margin") == 0) + { + if (!verify_int(chain_push(&chain, key), it.value)) + goto err; + } + + else if (strcmp(key, "location") == 0) { + if (!verify_enum(chain_push(&chain, key), it.value, + (const char *[]){"top", "bottom"}, 2)) + goto err; + } + + else if (strcmp(key, "background") == 0) { + if (!verify_color(chain_push(&chain, key), it.value)) + goto err; + } + + else if (strcmp(key, "border") == 0) { + if (!verify_border(chain_push(&chain, key), it.value)) + goto err; + } + + else if (strcmp(key, "font") == 0) { + if (!verify_font(chain_push(&chain, key), it.value)) + goto err; + } + + else if (strcmp(key, "left") == 0 || + strcmp(key, "center") == 0 || + strcmp(key, "right") == 0) + { + if (!verify_module_list(chain_push(&chain, key), it.value)) + goto err; + } + + else { + LOG_ERR("%s: invalid key: %s", err_prefix(&chain, bar), key); + goto err; + } + + LOG_DBG("%s: verified", key); + chain_pop(&chain); + } + + ret = true; + +err: + tll_free(chain); + return ret; +} diff --git a/config-verify.h b/config-verify.h new file mode 100644 index 0000000..92b3941 --- /dev/null +++ b/config-verify.h @@ -0,0 +1,5 @@ +#pragma once +#include +#include "yml.h" + +bool config_verify_bar(const struct yml_node *bar); diff --git a/config.c b/config.c index c46539d..c4568cd 100644 --- a/config.c +++ b/config.c @@ -33,6 +33,8 @@ #include "modules/xkb.h" #include "modules/xwindow.h" +#include "config-verify.h" + static uint8_t hex_nibble(char hex) { @@ -590,6 +592,9 @@ module_alsa_from_config(const struct yml_node *node, struct bar * conf_to_bar(const struct yml_node *bar) { + if (!config_verify_bar(bar)) + return NULL; + struct bar_config conf = {0}; /* Create a default font */ @@ -676,33 +681,36 @@ conf_to_bar(const struct yml_node *bar) it.node != NULL; yml_list_next(&it), idx++) { - const struct yml_node *n = yml_get_value(it.node, "module"); + assert(yml_is_dict(it.node)); + assert(yml_dict_length(it.node) == 1); - assert(n != NULL); - const char *mod_name = yml_value_as_string(n); + struct yml_dict_iter m = yml_dict_iter(it.node); + + const char *mod_name = yml_value_as_string(m.key); + assert(mod_name != NULL); if (strcmp(mod_name, "label") == 0) - mods[idx] = module_label_from_config(it.node, font); + mods[idx] = module_label_from_config(m.value, font); else if (strcmp(mod_name, "clock") == 0) - mods[idx] = module_clock_from_config(it.node, font); + mods[idx] = module_clock_from_config(m.value, font); else if (strcmp(mod_name, "xwindow") == 0) - mods[idx] = module_xwindow_from_config(it.node, font); + mods[idx] = module_xwindow_from_config(m.value, font); else if (strcmp(mod_name, "i3") == 0) - mods[idx] = module_i3_from_config(it.node, font); + mods[idx] = module_i3_from_config(m.value, font); else if (strcmp(mod_name, "battery") == 0) - mods[idx] = module_battery_from_config(it.node, font); + mods[idx] = module_battery_from_config(m.value, font); else if (strcmp(mod_name, "xkb") == 0) - mods[idx] = module_xkb_from_config(it.node, font); + mods[idx] = module_xkb_from_config(m.value, font); else if (strcmp(mod_name, "backlight") == 0) - mods[idx] = module_backlight_from_config(it.node, font); + mods[idx] = module_backlight_from_config(m.value, font); else if (strcmp(mod_name, "mpd") == 0) - mods[idx] = module_mpd_from_config(it.node, font); + mods[idx] = module_mpd_from_config(m.value, font); else if (strcmp(mod_name, "network") == 0) - mods[idx] = module_network_from_config(it.node, font); + mods[idx] = module_network_from_config(m.value, font); else if (strcmp(mod_name, "removables") == 0) - mods[idx] = module_removables_from_config(it.node, font); + mods[idx] = module_removables_from_config(m.value, font); else if (strcmp(mod_name, "alsa") == 0) - mods[idx] = module_alsa_from_config(it.node, font); + mods[idx] = module_alsa_from_config(m.value, font); else assert(false); } diff --git a/main.c b/main.c index 3507c42..f9164d7 100644 --- a/main.c +++ b/main.c @@ -82,7 +82,12 @@ main(int argc, const char *const *argv) return 1; } - free(config_path); + const struct yml_node *bar_conf = yml_get_value(conf, "bar"); + if (bar_conf == NULL) { + LOG_ERR("%s: missing required top level key 'bar'", config_path); + free(config_path); + return 1; + } xcb_init(); @@ -92,10 +97,18 @@ main(int argc, const char *const *argv) int abort_fd = eventfd(0, EFD_CLOEXEC); if (abort_fd == -1) { LOG_ERRNO("failed to create eventfd (for abort signalling)"); + free(config_path); return 1; } - struct bar *bar = conf_to_bar(yml_get_value(conf, "bar")); + struct bar *bar = conf_to_bar(bar_conf); + if (bar == NULL) { + LOG_ERR("%s: failed to load configuration", config_path); + free(config_path); + return 1; + } + + free(config_path); struct bar_run_context bar_ctx = { .bar = bar, diff --git a/yml.c b/yml.c index bfed04f..6de573b 100644 --- a/yml.c +++ b/yml.c @@ -682,42 +682,79 @@ yml_dict_length(const struct yml_node *dict) const char * yml_value_as_string(const struct yml_node *value) { - assert(yml_is_scalar(value)); + if (!yml_is_scalar(value)) + return NULL; return value->scalar.value; } +static bool +_as_int(const struct yml_node *value, long *ret) +{ + const char *s = yml_value_as_string(value); + if (s == NULL) + return false; + + int cnt; + int res = sscanf(s, "%ld%n", ret, &cnt); + return res == 1 && strlen(s) == (size_t)cnt; +} + +bool +yml_value_is_int(const struct yml_node *value) +{ + long dummy; + return _as_int(value, &dummy); +} + long yml_value_as_int(const struct yml_node *value) { - assert(yml_is_scalar(value)); - - long ival; - int res = sscanf(yml_value_as_string(value), "%ld", &ival); - return res != 1 ? -1 : ival; + long ret = -1; + _as_int(value, &ret); + return ret; } -bool -yml_value_as_bool(const struct yml_node *value) +static bool +_as_bool(const struct yml_node *value, bool *ret) { + if (!yml_is_scalar(value)) + return false; + const char *v = yml_value_as_string(value); if (strcasecmp(v, "y") == 0 || strcasecmp(v, "yes") == 0 || strcasecmp(v, "true") == 0 || strcasecmp(v, "on") == 0) { + *ret = true; return true; } else if (strcasecmp(v, "n") == 0 || strcasecmp(v, "no") == 0 || strcasecmp(v, "false") == 0 || strcasecmp(v, "off") == 0) { - return false; - } else - assert(false); + *ret = false; + return true; + } return false; } +bool +yml_value_is_bool(const struct yml_node *value) +{ + bool dummy; + return _as_bool(value, &dummy); +} + +bool +yml_value_as_bool(const struct yml_node *value) +{ + bool ret; + _as_bool(value, &ret); + return ret; +} + static void _print_node(const struct yml_node *n, int indent) { diff --git a/yml.h b/yml.h index 51ec58c..489b73a 100644 --- a/yml.h +++ b/yml.h @@ -32,6 +32,9 @@ struct yml_dict_iter yml_dict_iter(const struct yml_node *dict); void yml_dict_next(struct yml_dict_iter *iter); size_t yml_dict_length(const struct yml_node *dict); +bool yml_value_is_int(const struct yml_node *value); +bool yml_value_is_bool(const struct yml_node *value); + const char *yml_value_as_string(const struct yml_node *value); long yml_value_as_int(const struct yml_node *value); bool yml_value_as_bool(const struct yml_node *value);