diff --git a/README.md b/README.md index ede0fab..a03f3e9 100644 --- a/README.md +++ b/README.md @@ -142,8 +142,9 @@ a string (submitted with a newline). If the user were to enter `fieldvalue` in response to the prompt, the `runner` script would then have access to an environment variable `FIELDNAME` set to `fieldvalue`. Field names should respect the [POSIX standard](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html) -on environment variable naming. In particular, all field names consist solely -of alphanumeric characters or underscores and cannot start with a digit. +on environment variable naming. That is, field names must consist solely of +alphanumeric characters or underscores and are not permitted to start with a +digit. #### Types @@ -151,8 +152,18 @@ The value of `type` determines how a field is prompted for. Note the value of `type` is case insenstive. The currently supported list of types are: * `line` - * The simplest prompt type. Takes in a free-form response submitted after a - newline (`\n`) is encountered. + * The simplest prompt type. Takes in a free-form response submitted after + encountering a newline (`\n`). The resulting environment variable has + leading and trailing whitespace trimmed. + * If `required`, whitespace-only strings are re-prompted. + +#### Options + +Additional options can be included in a field definition. + +* `required` + * A value must be specified. How this option is interpreted depends on `type`. + * Defaults to `true`. #### Root Directory diff --git a/include/console.h b/include/console.h new file mode 100644 index 0000000..c0d3dd3 --- /dev/null +++ b/include/console.h @@ -0,0 +1,31 @@ +/** +@file +@brief Console output. +*/ +#ifndef _BOOTSTRAP_CONSOLE_H +#define _BOOTSTRAP_CONSOLE_H + +// clang-format off + +#define ANSI_BLACK "\e[0;30m" +#define ANSI_RED "\e[0;31m" +#define ANSI_GREEN "\e[0;32m" +#define ANSI_YELLOW "\e[0;33m" +#define ANSI_BLUE "\e[0;34m" +#define ANSI_PURPLE "\e[0;35m" +#define ANSI_CYAN "\e[0;36m" +#define ANSI_WHITE "\e[0;37m" +#define ANSI_RESET "\e[0m" + +#define ANSI_BLACK_F(...) ANSI_BLACK , __VA_ARGS__, ANSI_RESET +#define ANSI_RED_F(...) ANSI_RED , __VA_ARGS__, ANSI_RESET +#define ANSI_GREEN_F(...) ANSI_GREEN , __VA_ARGS__, ANSI_RESET +#define ANSI_YELLOW_F(...) ANSI_YELLOW, __VA_ARGS__, ANSI_RESET +#define ANSI_BLUE_F(...) ANSI_BLUE , __VA_ARGS__, ANSI_RESET +#define ANSI_PURPLE_F(...) ANSI_PURPLE, __VA_ARGS__, ANSI_RESET +#define ANSI_CYAN_F(...) ANSI_CYAN , __VA_ARGS__, ANSI_RESET +#define ANSI_WHITE_F(...) ANSI_WHITE , __VA_ARGS__, ANSI_RESET + +// clang-format on + +#endif /* _BOOTSTRAP_CONSOLE_H */ diff --git a/include/error.h b/include/error.h index 19b053f..fdfc958 100644 --- a/include/error.h +++ b/include/error.h @@ -40,12 +40,14 @@ enum ErrorCode { /// A field name in `spec.json` is not alphanumeric and beginning with a /// non-digit. ERROR_VALIDATOR_FIELD_NAME_INVALID, - /// The `type` of a `spec.json` field is not a string. + /// The `type` field of a `spec.json` file is not a string. ERROR_VALIDATOR_FIELD_TYPE_INVALID, - /// The `type` of a `spec.json` field does not correspond to a known prompt - /// type. + /// The `type` field of a `spec.json` file does not correspond to a known + /// prompt type. ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, - /// The `prompt` of a `spec.json` field is not a string. + /// The `required` field of a `spec.json` file is not a boolean. + ERROR_VALIDATOR_FIELD_REQUIRED_INVALID, + /// The `prompt` field of a `spec.json` file is not a string. ERROR_VALIDATOR_FIELD_PROMPT_INVALID, /// The `runner` file could not be found. @@ -75,7 +77,7 @@ static inline struct Error *priv_error_new( for (int i = 0; i < n; ++i) { string_buf_sappend(sb, messages[i]); } - e->message = string_buf_convert(sb); + e->message = string_buf_cast(sb); return e; } @@ -126,15 +128,6 @@ It is the responsibility of the caller to free the @ref Error instance. // clang-format on -#define ANSI_BLACK(...) "\e[0;30m", __VA_ARGS__, "\e[0m" -#define ANSI_RED(...) "\e[0;31m", __VA_ARGS__, "\e[0m" -#define ANSI_GREEN(...) "\e[0;32m", __VA_ARGS__, "\e[0m" -#define ANSI_YELLOW(...) "\e[0;33m", __VA_ARGS__, "\e[0m" -#define ANSI_BLUE(...) "\e[0;34m", __VA_ARGS__, "\e[0m" -#define ANSI_PURPLE(...) "\e[0;35m", __VA_ARGS__, "\e[0m" -#define ANSI_CYAN(...) "\e[0;36m", __VA_ARGS__, "\e[0m" -#define ANSI_WHITE(...) "\e[0;37m", __VA_ARGS__, "\e[0m" - /** @brief Deallocates a previously allocated @ref Error isntance. diff --git a/include/string_buf.h b/include/string_buf.h index 156e761..7c9ec53 100644 --- a/include/string_buf.h +++ b/include/string_buf.h @@ -75,7 +75,7 @@ of the internal array the necessary number of times to accommodate. void string_buf_sappend(struct StringBuf *sb, const char s[static 1]); /** -@brief Converts a @ref StringBuf instance into a `char*`. +@brief Casts a @ref StringBuf instance into a `char*`. This function frees the memory associated with @p sb. @@ -85,7 +85,7 @@ This function frees the memory associated with @p sb. A null pointer if @p sb is null. Otherwise a NUL-terminated string corresponding to the value of @p sb. The caller takes ownership of this value. */ -const char *string_buf_convert(struct StringBuf *sb); +const char *string_buf_cast(struct StringBuf *sb); /** @brief Deallocates a previously allocated @ref StringBuf instance. diff --git a/include/string_utils.h b/include/string_utils.h index e7a6d20..1095d14 100644 --- a/include/string_utils.h +++ b/include/string_utils.h @@ -42,4 +42,20 @@ This function operates like `strcmp` except comparison ignores case. */ int strcmp_ci(const char *s1, const char *s2); +/** +Removes any leading whitespace characters from the string in-place. + +@param s + The string to trim the start of. +*/ +void trim_leading(char *s); + +/** +Removes any tailing whitespace characters from the string in-place. + +@param s + The string to trim the end of. +*/ +void trim_trailing(char *s); + #endif /* _BOOTSTRAP_STRING_UTILS_H */ diff --git a/include/validator.h b/include/validator.h index 353ddb7..dc2fd8a 100644 --- a/include/validator.h +++ b/include/validator.h @@ -5,6 +5,8 @@ #ifndef _BOOTSTRAP_VALIDATOR_H #define _BOOTSTRAP_VALIDATOR_H +#include + #include "cJSON.h" #include "config.h" #include "dyn_array.h" @@ -42,6 +44,8 @@ struct Field { /// @brief The type of field. Denotes what prompt should be displayed prior to /// evaluation. enum FieldType type; + /// @brief Indicates the field is required. + bool required; /// @brief A reference to the name of the field. Does not take ownership of /// this value. const char *key; diff --git a/specs/mix/runner b/specs/mix/runner index 66936e0..135a943 100755 --- a/specs/mix/runner +++ b/specs/mix/runner @@ -39,8 +39,6 @@ trap cleanup EXIT # ENVIRONMENT # ============================================================ -# Trims away any whitespace around the module name. -MODULE=$(echo "$MODULE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') if [ -z "$MODULE" ]; then MODULE_ARG= else diff --git a/specs/mix/spec.json b/specs/mix/spec.json index cca219f..d8f64ed 100644 --- a/specs/mix/spec.json +++ b/specs/mix/spec.json @@ -1,10 +1,11 @@ { "app": { "type": "line", - "prompt": "App Name> " + "prompt": "App> " }, "module": { "type": "line", - "prompt": "Module (optional)> " + "required": false, + "prompt": "Module> " } } diff --git a/specs/phoenix/runner b/specs/phoenix/runner index 4bd025a..ad0b4eb 100755 --- a/specs/phoenix/runner +++ b/specs/phoenix/runner @@ -39,8 +39,6 @@ trap cleanup EXIT # ENVIRONMENT # ============================================================ -# Trims away any whitespace around the module name. -MODULE=$(echo "$MODULE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') if [ -z "$MODULE" ]; then MODULE_ARG= else diff --git a/specs/phoenix/spec.json b/specs/phoenix/spec.json index cca219f..d8f64ed 100644 --- a/specs/phoenix/spec.json +++ b/specs/phoenix/spec.json @@ -1,10 +1,11 @@ { "app": { "type": "line", - "prompt": "App Name> " + "prompt": "App> " }, "module": { "type": "line", - "prompt": "Module (optional)> " + "required": false, + "prompt": "Module> " } } diff --git a/src/config.c b/src/config.c index 2b6157d..9428bec 100644 --- a/src/config.c +++ b/src/config.c @@ -6,6 +6,7 @@ #include #include +#include "console.h" #include "string_utils.h" struct Error *config_new( @@ -19,16 +20,16 @@ struct Error *config_new( if (cwd == 0) { return ERROR_NEW( ERROR_CONFIG_ENV_CWD_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Could not retrieve ", - ANSI_CYAN("CWD"), + ANSI_CYAN_F("CWD"), "." ); } if (root_dir == 0) { return ERROR_NEW( ERROR_CONFIG_ENV_ROOT_DIR_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Could not find root directory." ); } @@ -44,17 +45,17 @@ struct Error *config_new( if (errno == ENOENT) { error = ERROR_NEW( ERROR_CONFIG_TARGET_NOT_FOUND, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Could not find ", - ANSI_BLUE(target), + ANSI_BLUE_F(target), " spec." ); } else { error = ERROR_NEW( ERROR_CONFIG_TARGET_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": ", - ANSI_BLUE(target), + ANSI_BLUE_F(target), " is invalid." ); } @@ -63,9 +64,9 @@ struct Error *config_new( if (!S_ISDIR(sb.st_mode)) { error = ERROR_NEW( ERROR_CONFIG_TARGET_NOT_DIR, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": ", - ANSI_CYAN(filepath), + ANSI_CYAN_F(filepath), " is not a directory." ); goto cleanup; diff --git a/src/dyn_array.c b/src/dyn_array.c index 1fbb8ae..e338925 100644 --- a/src/dyn_array.c +++ b/src/dyn_array.c @@ -4,10 +4,10 @@ struct DynArray *dyn_array_new(size_t capacity) { struct DynArray *a = malloc(sizeof(struct DynArray)); - size_t new_capacity = capacity ? capacity : 1; - a->buf = calloc(new_capacity, sizeof(void *)); + a->_capacity = (capacity == 0) ? 1 : capacity; + a->buf = malloc(a->_capacity * sizeof(void *)); + a->buf[0] = 0; a->size = 0; - a->_capacity = new_capacity; return a; } diff --git a/src/evaluator.c b/src/evaluator.c index 103d674..7a7b3ab 100644 --- a/src/evaluator.c +++ b/src/evaluator.c @@ -7,10 +7,13 @@ #include #include +#include "console.h" #include "string_buf.h" #include "string_utils.h" #include "validator.h" +#define BANNER_LENGTH 60 + static struct Error *find_run_exec(const struct Config *const config) { assert(config); @@ -23,9 +26,9 @@ static struct Error *find_run_exec(const struct Config *const config) { if (stat_res == -1 && errno == ENOENT) { return ERROR_NEW( ERROR_EVALUATOR_RUNNER_NOT_FOUND, - ANSI_RED("NOT_FOUND"), + ANSI_RED_F("NOT_FOUND"), ": Could not find ", - ANSI_BLUE(config->target, "/runner"), + ANSI_BLUE_F(config->target, "/runner"), "." ); } @@ -33,9 +36,9 @@ static struct Error *find_run_exec(const struct Config *const config) { if (!(sb.st_mode & S_IXUSR)) { return ERROR_NEW( ERROR_EVALUATOR_RUNNER_NOT_EXEC, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": ", - ANSI_BLUE(config->target, "/runner"), + ANSI_BLUE_F(config->target, "/runner"), " is not executable." ); } @@ -43,38 +46,105 @@ static struct Error *find_run_exec(const struct Config *const config) { return 0; } -static const char *prompt_field(struct Field *field) { - assert(field); - printf("%s", field->prompt); +static void print_header(const struct Config *const config) { + assert(config); + struct StringBuf *banner = string_buf_new(BANNER_LENGTH * 2); + for (int i = 0; i < BANNER_LENGTH; ++i) { + string_buf_cappend(banner, '='); + } + + struct StringBuf *header = string_buf_new(128); + string_buf_sappend(header, config->target); + string_buf_sappend(header, "/spec.json"); + int left_padding = (BANNER_LENGTH - header->size) / 2; + + printf("%s\n", banner->buf); + printf("%*s", left_padding, ""); + printf("%s%s/spec.json%s\n\n", ANSI_BLUE_F(config->target)); + printf("(%s%s%s) indicates a required field.\n", ANSI_YELLOW_F("*")); + printf("%s\n\n", banner->buf); + + string_buf_free(header); + string_buf_free(banner); +} + +static void print_prompt(const struct Field *const field) { + assert(field); + + if (field->required) { + printf("%s%s%s%s", ANSI_YELLOW_F("*"), field->prompt); + } else { + printf("%s", field->prompt); + } +} + +static const char *query_line(const struct Field *const field) { + assert(field); + + // TODO: Dynamically size this value. char *response = calloc(1, 1024); - switch (field->type) { - case FT_LINE: - // TODO: Probably want this buffer size to be a bit more dynamic. + do { + print_prompt(field); if (fgets(response, 1024, stdin)) { - size_t len = strlen(response); - if (len > 0 && response[len - 1] == '\n') { - response[len - 1] = '\0'; + trim_leading(response); + trim_trailing(response); + if (response[0] != 0) { + return response; } - return response; - } else { - free(response); - return 0; + } else { // Likely EOF. Force-quit even if required. + printf("\n"); + break; } - } + } while (field->required); + + free(response); + return 0; } static void push_env( struct StringBuf *env, const char *key, const char *value ) { assert(env); + for (const char *c = key; *c; ++c) { string_buf_cappend(env, toupper(*c)); } - string_buf_sappend(env, "='"); - string_buf_sappend(env, value); - string_buf_sappend(env, "' "); + string_buf_sappend(env, "="); + + if (value) { + string_buf_cappend(env, '\''); + string_buf_sappend(env, value); + string_buf_sappend(env, "' "); + } else { + string_buf_cappend(env, ' '); + } +} + +static struct Error *push_fields( + const struct Config *const config, + const struct DynArray *const fields, + struct StringBuf **env_buf +) { + for (int i = 0; i < fields->size; ++i) { + struct Field *field = fields->buf[i]; + const char *response = 0; + switch (field->type) { + case FT_LINE: + response = query_line(field); + break; + } + if (field->required && !response) { + return ERROR_NEW( + ERROR_EVALUATOR_RESPONSE_INVALID, + ANSI_RED_F("ERROR"), + ": Could not read response." + ); + } + push_env(*env_buf, field->key, response); + } + return 0; } int evaluate_runner( @@ -82,34 +152,23 @@ int evaluate_runner( const struct DynArray *const fields, struct Error **error ) { - *error = find_run_exec(config); - if (*error) { + if ((*error = find_run_exec(config))) { return EXIT_FAILURE; } struct StringBuf *env_buf = string_buf_new(512); push_env(env_buf, "OUT", config->cwd); - if (fields) { - for (int i = 0; i < fields->size; ++i) { - struct Field *field = fields->buf[i]; - const char *response = prompt_field(field); - if (!response) { - *error = ERROR_NEW( - ERROR_EVALUATOR_RESPONSE_INVALID, - ANSI_RED("ERROR"), - ": Could not read response." - ); - string_buf_free(env_buf); - return EXIT_FAILURE; - } - push_env(env_buf, field->key, response); + print_header(config); + if ((*error = push_fields(config, fields, &env_buf))) { + string_buf_free(env_buf); + return EXIT_FAILURE; } } const char *segments[] = {config->root_dir, config->target, "runner"}; const char *filepath = join(sizeof(segments) / sizeof(char *), segments, '/'); - const char *env = string_buf_convert(env_buf); + const char *env = string_buf_cast(env_buf); struct StringBuf *command_buf = string_buf_new(1024); string_buf_sappend(command_buf, "cd "); @@ -119,7 +178,7 @@ int evaluate_runner( string_buf_sappend(command_buf, " && "); string_buf_sappend(command_buf, env); string_buf_sappend(command_buf, filepath); - const char *command = string_buf_convert(command_buf); + const char *command = string_buf_cast(command_buf); free((void *)env); free((void *)filepath); diff --git a/src/parser.c b/src/parser.c index ef86e8f..a0249d5 100644 --- a/src/parser.c +++ b/src/parser.c @@ -5,6 +5,7 @@ #include #include +#include "console.h" #include "string_utils.h" static struct Error *find_spec_json( @@ -20,9 +21,9 @@ static struct Error *find_spec_json( if (!*handle && errno != ENOENT) { error = ERROR_NEW( ERROR_PARSER_SPEC_JSON_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " is invalid." ); } @@ -64,9 +65,9 @@ struct Error *parse_spec_json( if (!*parsed) { return ERROR_NEW( ERROR_PARSER_SPEC_JSON_INVALID_SYNTAX, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " contains invalid JSON." ); } diff --git a/src/string_buf.c b/src/string_buf.c index 064c8b0..9365304 100644 --- a/src/string_buf.c +++ b/src/string_buf.c @@ -7,9 +7,10 @@ struct StringBuf *string_buf_new(size_t capacity) { struct StringBuf *sb = malloc(sizeof(struct StringBuf)); - sb->buf = calloc(capacity, sizeof(char)); + sb->_capacity = (capacity == 0) ? 1 : capacity; + sb->buf = malloc(sb->_capacity * sizeof(char)); + sb->buf[0] = 0; sb->size = 0; - sb->_capacity = capacity; return sb; } @@ -21,13 +22,11 @@ size_t string_buf_size(struct StringBuf *sb) { void string_buf_cappend(struct StringBuf *sb, char c) { assert(sb); - if (sb->_capacity) { + if (sb->size + 1 + 1 > sb->_capacity) { // size + NUL + c sb->_capacity *= 2; - } else { - sb->_capacity = 2; + sb->buf = realloc((void *)sb->buf, sb->_capacity); } - sb->buf = realloc((void *)sb->buf, sb->_capacity); sb->buf[sb->size++] = c; sb->buf[sb->size] = 0; } @@ -35,24 +34,20 @@ void string_buf_cappend(struct StringBuf *sb, char c) { void string_buf_sappend(struct StringBuf *sb, const char s[static 1]) { assert(sb); - double goal = sb->size + strlen(s) + 1; - double denom = sb->_capacity ? sb->_capacity : 1; - double scale = pow(2, ceil(log2(goal / denom))); - - if (sb->_capacity) { - sb->_capacity *= scale; - } else { - sb->_capacity = scale; + size_t slen = strlen(s); + double goal = sb->size + 1 + slen; + if (goal > sb->_capacity) { // size + NUL + slen + sb->_capacity *= pow(2, ceil(log2(goal / sb->_capacity))); + sb->buf = realloc((void *)sb->buf, sb->_capacity); } - sb->buf = realloc((void *)sb->buf, sb->_capacity); - for (const char *i = s; *i; ++i) { - sb->buf[sb->size++] = *i; + for (const char *c = s; *c; ++c) { + sb->buf[sb->size++] = *c; } sb->buf[sb->size] = 0; } -const char *string_buf_convert(struct StringBuf *sb) { +const char *string_buf_cast(struct StringBuf *sb) { assert(sb); const char *buf = sb->buf; free(sb); diff --git a/src/string_utils.c b/src/string_utils.c index 332d81a..57c03c9 100644 --- a/src/string_utils.c +++ b/src/string_utils.c @@ -45,3 +45,33 @@ int strcmp_ci(const char *s1, const char *s2) { } return s1_len < s2_len ? -1 : 1; } + +void trim_leading(char *s) { + int count = 0; + for (const char *c = s; isspace(*c); ++c) { + count++; + } + if (count == 0) { + return; + } + // Shift elements down. + size_t len = strlen(s); + for (int i = 0; i < len - count + 1; ++i) { + s[i] = s[i + count]; + } +} + +void trim_trailing(char *s) { + int last = -1; + size_t len = strlen(s); + for (int i = 0; i < len; ++i) { + if (!isspace(s[i])) { + last = i; + } + } + if (last == -1) { + s[0] = 0; + } else if (last < len - 1) { + s[last + 1] = 0; + } +} diff --git a/src/validator.c b/src/validator.c index 8933920..4b3d7e3 100644 --- a/src/validator.c +++ b/src/validator.c @@ -2,6 +2,7 @@ #include +#include "console.h" #include "string_utils.h" static struct Error *read_field( @@ -12,11 +13,11 @@ static struct Error *read_field( if (!cJSON_IsObject(field)) { return ERROR_NEW( ERROR_VALIDATOR_FIELD_NOT_OBJECT, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Field ", - ANSI_PURPLE(field->string), + ANSI_PURPLE_F(field->string), " in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " is not a JSON object." ); } @@ -24,11 +25,11 @@ static struct Error *read_field( if (isdigit(field->string[0])) { return ERROR_NEW( ERROR_VALIDATOR_FIELD_NAME_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Field ", - ANSI_PURPLE(field->string), + ANSI_PURPLE_F(field->string), " in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " may not begin with a digit." ); } else { @@ -36,11 +37,11 @@ static struct Error *read_field( if (*c != '_' && !isalnum(*c)) { return ERROR_NEW( ERROR_VALIDATOR_FIELD_NAME_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Field ", - ANSI_PURPLE(field->string), + ANSI_PURPLE_F(field->string), " in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " must consist of only alphanumeric characters and underscores." ); } @@ -55,13 +56,13 @@ static struct Error *read_field( if (!cJSON_IsString(type)) { error = ERROR_NEW( ERROR_VALIDATOR_FIELD_TYPE_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Field ", - ANSI_PURPLE(field->string), + ANSI_PURPLE_F(field->string), " in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " has non-string ", - ANSI_PURPLE("type"), + ANSI_PURPLE_F("type"), "." ); goto cleanup; @@ -72,13 +73,33 @@ static struct Error *read_field( } else { error = ERROR_NEW( ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Field ", - ANSI_PURPLE(field->string), + ANSI_PURPLE_F(field->string), " in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " has unknown ", - ANSI_PURPLE("type"), + ANSI_PURPLE_F("type"), + "." + ); + goto cleanup; + } + + const cJSON *required = cJSON_GetObjectItemCaseSensitive(field, "required"); + if (!required) { + (*out)->required = true; + } else if (cJSON_IsBool(required)) { + (*out)->required = required->valueint; + } else { + error = ERROR_NEW( + ERROR_VALIDATOR_FIELD_REQUIRED_INVALID, + ANSI_RED_F("ERROR"), + ": Field ", + ANSI_PURPLE_F(field->string), + " in ", + ANSI_BLUE_F(config->target, "/spec.json"), + " has non-boolean ", + ANSI_PURPLE_F("required"), "." ); goto cleanup; @@ -90,13 +111,13 @@ static struct Error *read_field( } else { error = ERROR_NEW( ERROR_VALIDATOR_FIELD_PROMPT_INVALID, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Field ", - ANSI_PURPLE(field->string), + ANSI_PURPLE_F(field->string), " in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " has non-string ", - ANSI_PURPLE("prompt"), + ANSI_PURPLE_F("prompt"), "." ); goto cleanup; @@ -124,9 +145,9 @@ struct Error *validate_spec_json( if (!cJSON_IsObject(parsed)) { return ERROR_NEW( ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT, - ANSI_RED("ERROR"), + ANSI_RED_F("ERROR"), ": Top-level JSON value in ", - ANSI_BLUE(config->target, "/spec.json"), + ANSI_BLUE_F(config->target, "/spec.json"), " is not an object." ); } diff --git a/test/suites.c b/test/suites.c index 95dce04..f4fdcf6 100644 --- a/test/suites.c +++ b/test/suites.c @@ -27,6 +27,8 @@ int main(int argc, char *argv[]) { sput_run_test(test_join_single); sput_run_test(test_join_multiple); sput_run_test(test_strcmp_ci); + sput_run_test(test_trim_leading); + sput_run_test(test_trim_trailing); sput_enter_suite("parser"); sput_run_test(test_parser_missing); @@ -41,8 +43,10 @@ int main(int argc, char *argv[]) { sput_run_test(test_validator_field_type_invalid); sput_run_test(test_validator_field_type_unknown); sput_run_test(test_validator_valid_type_ci); + sput_run_test(test_validator_field_required_invalid); + sput_run_test(test_validator_field_required_valid); sput_run_test(test_validator_field_prompt_invalid); - sput_run_test(test_validator_valid); + sput_run_test(test_validator_valid_no_required); sput_finish_testing(); diff --git a/test/test_string_buf.h b/test/test_string_buf.h index d56d20b..db776cf 100644 --- a/test/test_string_buf.h +++ b/test/test_string_buf.h @@ -12,10 +12,9 @@ static void test_string_buf_sappend() { sput_fail_unless( string_buf_size(sb) == strlen("hello world!!"), "sappend size" ); - const char *converted = string_buf_convert(sb); - sput_fail_unless( - strcmp(converted, "hello world!!") == 0, "sappend converted" - ); + const char *cast = string_buf_cast(sb); + sput_fail_unless(strcmp(cast, "hello world!!") == 0, "sappend cast"); + free((void *)cast); } static void test_string_buf_cappend() { @@ -27,10 +26,9 @@ static void test_string_buf_cappend() { sput_fail_unless( string_buf_size(sb) == strlen("hello world!!"), "cappend size" ); - const char *converted = string_buf_convert(sb); - sput_fail_unless( - strcmp(converted, "hello world!!") == 0, "cappend converted" - ); + const char *cast = string_buf_cast(sb); + sput_fail_unless(strcmp(cast, "hello world!!") == 0, "cappend cast"); + free((void *)cast); } static void test_string_buf_nonzero_capacity() { diff --git a/test/test_string_utils.h b/test/test_string_utils.h index 42f752a..9fc84cb 100644 --- a/test/test_string_utils.h +++ b/test/test_string_utils.h @@ -22,10 +22,51 @@ static void test_strcmp_ci() { const char *a1 = "aBcD"; const char *a2 = "AbCd"; sput_fail_unless(strcmp_ci(a1, a2) == 0, "strcmp_ci == 0"); + const char *b1 = "aBcDe"; const char *b2 = "AbCd"; sput_fail_unless(strcmp_ci(b1, b2) > 0, "strcmp_ci > 0"); sput_fail_unless(strcmp_ci(b2, b1) < 0, "strcmp_ci < 0"); } +static void test_trim_leading() { + char a1[] = {0}; + char a2[] = {' ', ' ', ' ', 0}; + trim_leading(a1); + trim_leading(a2); + sput_fail_unless(a1[0] == 0, "trim leading empty string"); + sput_fail_unless(strcmp(a1, a2) == 0, "trim leading whitespace string"); + + char b1[] = {'a', 'b', 'c', 'd', 'e', 'f', 0}; + char b2[] = {' ', ' ', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 0}; + trim_leading(b1); + trim_leading(b2); + sput_fail_unless(strcmp(b1, b2) == 0, "trim leading string"); + + char c1[] = {'a', 'b', 'c', 'd', 'e', 'f', ' ', ' ', ' ', 0}; + char c2[] = {'a', 'b', 'c', 'd', 'e', 'f', ' ', ' ', ' ', 0}; + trim_leading(c1); + sput_fail_unless(strcmp(c1, c2) == 0, "trim leading ignore trailing"); +} + +static void test_trim_trailing() { + char a1[] = {0}; + char a2[] = {' ', ' ', ' ', 0}; + trim_trailing(a1); + trim_trailing(a2); + sput_fail_unless(a1[0] == 0, "trim trailing empty string"); + sput_fail_unless(strcmp(a1, a2) == 0, "trim trailing whitespace string"); + + char b1[] = {'a', 'b', 'c', 'd', 'e', 'f', 0}; + char b2[] = {'a', 'b', 'c', 'd', 'e', 'f', ' ', ' ', ' ', 0}; + trim_trailing(b1); + trim_trailing(b2); + sput_fail_unless(strcmp(b1, b2) == 0, "trim trailing string"); + + char c1[] = {' ', ' ', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 0}; + char c2[] = {' ', ' ', ' ', 'a', 'b', 'c', 'd', 'e', 'f', 0}; + trim_trailing(c1); + sput_fail_unless(strcmp(c1, c2) == 0, "trim trailing ignore leading"); +} + #endif /* _BOOTSTRAP_TEST_STRING_UTILS */ diff --git a/test/test_validator.h b/test/test_validator.h index f46cab5..4b3248e 100644 --- a/test/test_validator.h +++ b/test/test_validator.h @@ -163,6 +163,46 @@ static void test_validator_valid_type_ci() { test_validator_teardown(fixture); } +static void test_validator_field_required_invalid() { + struct TestValidatorFixture *fixture = test_validator_setup( + "{" + " \"key\": {" + " \"type\": \"line\"," + " \"required\": 5" + " }" + "}" + ); + + struct Error *error = + validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts); + sput_fail_unless( + error->code == ERROR_VALIDATOR_FIELD_REQUIRED_INVALID, + "field required invalid" + ); + error_free(error); + + test_validator_teardown(fixture); +} + +static void test_validator_field_required_valid() { + struct TestValidatorFixture *fixture = test_validator_setup( + "{" + " \"key\": {" + " \"type\": \"line\"," + " \"required\": true," + " \"prompt\": \"What value for key? \"" + " }" + "}" + ); + + struct Error *error = + validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts); + sput_fail_unless(error == 0, "required valid"); + error_free(error); + + test_validator_teardown(fixture); +} + static void test_validator_field_prompt_invalid() { struct TestValidatorFixture *fixture = test_validator_setup( "{" @@ -183,7 +223,7 @@ static void test_validator_field_prompt_invalid() { test_validator_teardown(fixture); } -static void test_validator_valid() { +static void test_validator_valid_no_required() { struct TestValidatorFixture *fixture = test_validator_setup( "{" " \"key\": {"