Add an optional `required` attribute to fields. (#10)

pull/11/head v0.1.2
Joshua Potter 2023-11-30 09:33:25 -07:00 committed by GitHub
parent e987caf64b
commit 383ada8661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 377 additions and 134 deletions

View File

@ -142,8 +142,9 @@ a string (submitted with a newline).
If the user were to enter `fieldvalue` in response to the prompt, the `runner` 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 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) `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 on environment variable naming. That is, field names must consist solely of
of alphanumeric characters or underscores and cannot start with a digit. alphanumeric characters or underscores and are not permitted to start with a
digit.
#### Types #### 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: `type` is case insenstive. The currently supported list of types are:
* `line` * `line`
* The simplest prompt type. Takes in a free-form response submitted after a * The simplest prompt type. Takes in a free-form response submitted after
newline (`\n`) is encountered. 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 #### Root Directory

31
include/console.h Normal file
View File

@ -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 */

View File

@ -40,12 +40,14 @@ enum ErrorCode {
/// A field name in `spec.json` is not alphanumeric and beginning with a /// A field name in `spec.json` is not alphanumeric and beginning with a
/// non-digit. /// non-digit.
ERROR_VALIDATOR_FIELD_NAME_INVALID, 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, ERROR_VALIDATOR_FIELD_TYPE_INVALID,
/// The `type` of a `spec.json` field does not correspond to a known prompt /// The `type` field of a `spec.json` file does not correspond to a known
/// type. /// prompt type.
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, 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, ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
/// The `runner` file could not be found. /// 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) { for (int i = 0; i < n; ++i) {
string_buf_sappend(sb, messages[i]); string_buf_sappend(sb, messages[i]);
} }
e->message = string_buf_convert(sb); e->message = string_buf_cast(sb);
return e; return e;
} }
@ -126,15 +128,6 @@ It is the responsibility of the caller to free the @ref Error instance.
// clang-format on // 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. @brief Deallocates a previously allocated @ref Error isntance.

View File

@ -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]); 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. 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 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. 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. @brief Deallocates a previously allocated @ref StringBuf instance.

View File

@ -42,4 +42,20 @@ This function operates like `strcmp` except comparison ignores case.
*/ */
int strcmp_ci(const char *s1, const char *s2); 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 */ #endif /* _BOOTSTRAP_STRING_UTILS_H */

View File

@ -5,6 +5,8 @@
#ifndef _BOOTSTRAP_VALIDATOR_H #ifndef _BOOTSTRAP_VALIDATOR_H
#define _BOOTSTRAP_VALIDATOR_H #define _BOOTSTRAP_VALIDATOR_H
#include <stdbool.h>
#include "cJSON.h" #include "cJSON.h"
#include "config.h" #include "config.h"
#include "dyn_array.h" #include "dyn_array.h"
@ -42,6 +44,8 @@ struct Field {
/// @brief The type of field. Denotes what prompt should be displayed prior to /// @brief The type of field. Denotes what prompt should be displayed prior to
/// evaluation. /// evaluation.
enum FieldType type; 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 /// @brief A reference to the name of the field. Does not take ownership of
/// this value. /// this value.
const char *key; const char *key;

View File

@ -39,8 +39,6 @@ trap cleanup EXIT
# ENVIRONMENT # ENVIRONMENT
# ============================================================ # ============================================================
# Trims away any whitespace around the module name.
MODULE=$(echo "$MODULE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -z "$MODULE" ]; then if [ -z "$MODULE" ]; then
MODULE_ARG= MODULE_ARG=
else else

View File

@ -1,10 +1,11 @@
{ {
"app": { "app": {
"type": "line", "type": "line",
"prompt": "App Name> " "prompt": "App> "
}, },
"module": { "module": {
"type": "line", "type": "line",
"prompt": "Module (optional)> " "required": false,
"prompt": "Module> "
} }
} }

View File

@ -39,8 +39,6 @@ trap cleanup EXIT
# ENVIRONMENT # ENVIRONMENT
# ============================================================ # ============================================================
# Trims away any whitespace around the module name.
MODULE=$(echo "$MODULE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -z "$MODULE" ]; then if [ -z "$MODULE" ]; then
MODULE_ARG= MODULE_ARG=
else else

View File

@ -1,10 +1,11 @@
{ {
"app": { "app": {
"type": "line", "type": "line",
"prompt": "App Name> " "prompt": "App> "
}, },
"module": { "module": {
"type": "line", "type": "line",
"prompt": "Module (optional)> " "required": false,
"prompt": "Module> "
} }
} }

View File

@ -6,6 +6,7 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "console.h"
#include "string_utils.h" #include "string_utils.h"
struct Error *config_new( struct Error *config_new(
@ -19,16 +20,16 @@ struct Error *config_new(
if (cwd == 0) { if (cwd == 0) {
return ERROR_NEW( return ERROR_NEW(
ERROR_CONFIG_ENV_CWD_INVALID, ERROR_CONFIG_ENV_CWD_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Could not retrieve ", ": Could not retrieve ",
ANSI_CYAN("CWD"), ANSI_CYAN_F("CWD"),
"." "."
); );
} }
if (root_dir == 0) { if (root_dir == 0) {
return ERROR_NEW( return ERROR_NEW(
ERROR_CONFIG_ENV_ROOT_DIR_INVALID, ERROR_CONFIG_ENV_ROOT_DIR_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Could not find root directory." ": Could not find root directory."
); );
} }
@ -44,17 +45,17 @@ struct Error *config_new(
if (errno == ENOENT) { if (errno == ENOENT) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_CONFIG_TARGET_NOT_FOUND, ERROR_CONFIG_TARGET_NOT_FOUND,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Could not find ", ": Could not find ",
ANSI_BLUE(target), ANSI_BLUE_F(target),
" spec." " spec."
); );
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_CONFIG_TARGET_INVALID, ERROR_CONFIG_TARGET_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": ", ": ",
ANSI_BLUE(target), ANSI_BLUE_F(target),
" is invalid." " is invalid."
); );
} }
@ -63,9 +64,9 @@ struct Error *config_new(
if (!S_ISDIR(sb.st_mode)) { if (!S_ISDIR(sb.st_mode)) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_CONFIG_TARGET_NOT_DIR, ERROR_CONFIG_TARGET_NOT_DIR,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": ", ": ",
ANSI_CYAN(filepath), ANSI_CYAN_F(filepath),
" is not a directory." " is not a directory."
); );
goto cleanup; goto cleanup;

View File

@ -4,10 +4,10 @@
struct DynArray *dyn_array_new(size_t capacity) { struct DynArray *dyn_array_new(size_t capacity) {
struct DynArray *a = malloc(sizeof(struct DynArray)); struct DynArray *a = malloc(sizeof(struct DynArray));
size_t new_capacity = capacity ? capacity : 1; a->_capacity = (capacity == 0) ? 1 : capacity;
a->buf = calloc(new_capacity, sizeof(void *)); a->buf = malloc(a->_capacity * sizeof(void *));
a->buf[0] = 0;
a->size = 0; a->size = 0;
a->_capacity = new_capacity;
return a; return a;
} }

View File

@ -7,10 +7,13 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "console.h"
#include "string_buf.h" #include "string_buf.h"
#include "string_utils.h" #include "string_utils.h"
#include "validator.h" #include "validator.h"
#define BANNER_LENGTH 60
static struct Error *find_run_exec(const struct Config *const config) { static struct Error *find_run_exec(const struct Config *const config) {
assert(config); assert(config);
@ -23,9 +26,9 @@ static struct Error *find_run_exec(const struct Config *const config) {
if (stat_res == -1 && errno == ENOENT) { if (stat_res == -1 && errno == ENOENT) {
return ERROR_NEW( return ERROR_NEW(
ERROR_EVALUATOR_RUNNER_NOT_FOUND, ERROR_EVALUATOR_RUNNER_NOT_FOUND,
ANSI_RED("NOT_FOUND"), ANSI_RED_F("NOT_FOUND"),
": Could not find ", ": 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)) { if (!(sb.st_mode & S_IXUSR)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_EVALUATOR_RUNNER_NOT_EXEC, 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." " is not executable."
); );
} }
@ -43,38 +46,105 @@ static struct Error *find_run_exec(const struct Config *const config) {
return 0; return 0;
} }
static const char *prompt_field(struct Field *field) { static void print_header(const struct Config *const config) {
assert(field); assert(config);
printf("%s", field->prompt);
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); char *response = calloc(1, 1024);
switch (field->type) { do {
case FT_LINE: print_prompt(field);
// TODO: Probably want this buffer size to be a bit more dynamic.
if (fgets(response, 1024, stdin)) { if (fgets(response, 1024, stdin)) {
size_t len = strlen(response); trim_leading(response);
if (len > 0 && response[len - 1] == '\n') { trim_trailing(response);
response[len - 1] = '\0'; if (response[0] != 0) {
return response;
} }
return response; } else { // Likely EOF. Force-quit even if required.
} else { printf("\n");
free(response); break;
return 0;
} }
} } while (field->required);
free(response);
return 0;
} }
static void push_env( static void push_env(
struct StringBuf *env, const char *key, const char *value struct StringBuf *env, const char *key, const char *value
) { ) {
assert(env); assert(env);
for (const char *c = key; *c; ++c) { for (const char *c = key; *c; ++c) {
string_buf_cappend(env, toupper(*c)); string_buf_cappend(env, toupper(*c));
} }
string_buf_sappend(env, "='"); string_buf_sappend(env, "=");
string_buf_sappend(env, value);
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( int evaluate_runner(
@ -82,34 +152,23 @@ int evaluate_runner(
const struct DynArray *const fields, const struct DynArray *const fields,
struct Error **error struct Error **error
) { ) {
*error = find_run_exec(config); if ((*error = find_run_exec(config))) {
if (*error) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
struct StringBuf *env_buf = string_buf_new(512); struct StringBuf *env_buf = string_buf_new(512);
push_env(env_buf, "OUT", config->cwd); push_env(env_buf, "OUT", config->cwd);
if (fields) { if (fields) {
for (int i = 0; i < fields->size; ++i) { print_header(config);
struct Field *field = fields->buf[i]; if ((*error = push_fields(config, fields, &env_buf))) {
const char *response = prompt_field(field); string_buf_free(env_buf);
if (!response) { return EXIT_FAILURE;
*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);
} }
} }
const char *segments[] = {config->root_dir, config->target, "runner"}; const char *segments[] = {config->root_dir, config->target, "runner"};
const char *filepath = join(sizeof(segments) / sizeof(char *), segments, '/'); 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); struct StringBuf *command_buf = string_buf_new(1024);
string_buf_sappend(command_buf, "cd "); string_buf_sappend(command_buf, "cd ");
@ -119,7 +178,7 @@ int evaluate_runner(
string_buf_sappend(command_buf, " && "); string_buf_sappend(command_buf, " && ");
string_buf_sappend(command_buf, env); string_buf_sappend(command_buf, env);
string_buf_sappend(command_buf, filepath); 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 *)env);
free((void *)filepath); free((void *)filepath);

View File

@ -5,6 +5,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include "console.h"
#include "string_utils.h" #include "string_utils.h"
static struct Error *find_spec_json( static struct Error *find_spec_json(
@ -20,9 +21,9 @@ static struct Error *find_spec_json(
if (!*handle && errno != ENOENT) { if (!*handle && errno != ENOENT) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_PARSER_SPEC_JSON_INVALID, 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." " is invalid."
); );
} }
@ -64,9 +65,9 @@ struct Error *parse_spec_json(
if (!*parsed) { if (!*parsed) {
return ERROR_NEW( return ERROR_NEW(
ERROR_PARSER_SPEC_JSON_INVALID_SYNTAX, 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." " contains invalid JSON."
); );
} }

View File

@ -7,9 +7,10 @@
struct StringBuf *string_buf_new(size_t capacity) { struct StringBuf *string_buf_new(size_t capacity) {
struct StringBuf *sb = malloc(sizeof(struct StringBuf)); 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->size = 0;
sb->_capacity = capacity;
return sb; return sb;
} }
@ -21,13 +22,11 @@ size_t string_buf_size(struct StringBuf *sb) {
void string_buf_cappend(struct StringBuf *sb, char c) { void string_buf_cappend(struct StringBuf *sb, char c) {
assert(sb); assert(sb);
if (sb->_capacity) { if (sb->size + 1 + 1 > sb->_capacity) { // size + NUL + c
sb->_capacity *= 2; sb->_capacity *= 2;
} else { sb->buf = realloc((void *)sb->buf, sb->_capacity);
sb->_capacity = 2;
} }
sb->buf = realloc((void *)sb->buf, sb->_capacity);
sb->buf[sb->size++] = c; sb->buf[sb->size++] = c;
sb->buf[sb->size] = 0; 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]) { void string_buf_sappend(struct StringBuf *sb, const char s[static 1]) {
assert(sb); assert(sb);
double goal = sb->size + strlen(s) + 1; size_t slen = strlen(s);
double denom = sb->_capacity ? sb->_capacity : 1; double goal = sb->size + 1 + slen;
double scale = pow(2, ceil(log2(goal / denom))); if (goal > sb->_capacity) { // size + NUL + slen
sb->_capacity *= pow(2, ceil(log2(goal / sb->_capacity)));
if (sb->_capacity) { sb->buf = realloc((void *)sb->buf, sb->_capacity);
sb->_capacity *= scale;
} else {
sb->_capacity = scale;
} }
sb->buf = realloc((void *)sb->buf, sb->_capacity); for (const char *c = s; *c; ++c) {
for (const char *i = s; *i; ++i) { sb->buf[sb->size++] = *c;
sb->buf[sb->size++] = *i;
} }
sb->buf[sb->size] = 0; sb->buf[sb->size] = 0;
} }
const char *string_buf_convert(struct StringBuf *sb) { const char *string_buf_cast(struct StringBuf *sb) {
assert(sb); assert(sb);
const char *buf = sb->buf; const char *buf = sb->buf;
free(sb); free(sb);

View File

@ -45,3 +45,33 @@ int strcmp_ci(const char *s1, const char *s2) {
} }
return s1_len < s2_len ? -1 : 1; 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;
}
}

View File

@ -2,6 +2,7 @@
#include <ctype.h> #include <ctype.h>
#include "console.h"
#include "string_utils.h" #include "string_utils.h"
static struct Error *read_field( static struct Error *read_field(
@ -12,11 +13,11 @@ static struct Error *read_field(
if (!cJSON_IsObject(field)) { if (!cJSON_IsObject(field)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NOT_OBJECT, ERROR_VALIDATOR_FIELD_NOT_OBJECT,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Field ", ": Field ",
ANSI_PURPLE(field->string), ANSI_PURPLE_F(field->string),
" in ", " in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" is not a JSON object." " is not a JSON object."
); );
} }
@ -24,11 +25,11 @@ static struct Error *read_field(
if (isdigit(field->string[0])) { if (isdigit(field->string[0])) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NAME_INVALID, ERROR_VALIDATOR_FIELD_NAME_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Field ", ": Field ",
ANSI_PURPLE(field->string), ANSI_PURPLE_F(field->string),
" in ", " in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" may not begin with a digit." " may not begin with a digit."
); );
} else { } else {
@ -36,11 +37,11 @@ static struct Error *read_field(
if (*c != '_' && !isalnum(*c)) { if (*c != '_' && !isalnum(*c)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NAME_INVALID, ERROR_VALIDATOR_FIELD_NAME_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Field ", ": Field ",
ANSI_PURPLE(field->string), ANSI_PURPLE_F(field->string),
" in ", " in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" must consist of only alphanumeric characters and underscores." " must consist of only alphanumeric characters and underscores."
); );
} }
@ -55,13 +56,13 @@ static struct Error *read_field(
if (!cJSON_IsString(type)) { if (!cJSON_IsString(type)) {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_INVALID, ERROR_VALIDATOR_FIELD_TYPE_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Field ", ": Field ",
ANSI_PURPLE(field->string), ANSI_PURPLE_F(field->string),
" in ", " in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" has non-string ", " has non-string ",
ANSI_PURPLE("type"), ANSI_PURPLE_F("type"),
"." "."
); );
goto cleanup; goto cleanup;
@ -72,13 +73,33 @@ static struct Error *read_field(
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Field ", ": Field ",
ANSI_PURPLE(field->string), ANSI_PURPLE_F(field->string),
" in ", " in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" has unknown ", " 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; goto cleanup;
@ -90,13 +111,13 @@ static struct Error *read_field(
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_PROMPT_INVALID, ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Field ", ": Field ",
ANSI_PURPLE(field->string), ANSI_PURPLE_F(field->string),
" in ", " in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" has non-string ", " has non-string ",
ANSI_PURPLE("prompt"), ANSI_PURPLE_F("prompt"),
"." "."
); );
goto cleanup; goto cleanup;
@ -124,9 +145,9 @@ struct Error *validate_spec_json(
if (!cJSON_IsObject(parsed)) { if (!cJSON_IsObject(parsed)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT, ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT,
ANSI_RED("ERROR"), ANSI_RED_F("ERROR"),
": Top-level JSON value in ", ": Top-level JSON value in ",
ANSI_BLUE(config->target, "/spec.json"), ANSI_BLUE_F(config->target, "/spec.json"),
" is not an object." " is not an object."
); );
} }

View File

@ -27,6 +27,8 @@ int main(int argc, char *argv[]) {
sput_run_test(test_join_single); sput_run_test(test_join_single);
sput_run_test(test_join_multiple); sput_run_test(test_join_multiple);
sput_run_test(test_strcmp_ci); sput_run_test(test_strcmp_ci);
sput_run_test(test_trim_leading);
sput_run_test(test_trim_trailing);
sput_enter_suite("parser"); sput_enter_suite("parser");
sput_run_test(test_parser_missing); 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_invalid);
sput_run_test(test_validator_field_type_unknown); sput_run_test(test_validator_field_type_unknown);
sput_run_test(test_validator_valid_type_ci); 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_field_prompt_invalid);
sput_run_test(test_validator_valid); sput_run_test(test_validator_valid_no_required);
sput_finish_testing(); sput_finish_testing();

View File

@ -12,10 +12,9 @@ static void test_string_buf_sappend() {
sput_fail_unless( sput_fail_unless(
string_buf_size(sb) == strlen("hello world!!"), "sappend size" string_buf_size(sb) == strlen("hello world!!"), "sappend size"
); );
const char *converted = string_buf_convert(sb); const char *cast = string_buf_cast(sb);
sput_fail_unless( sput_fail_unless(strcmp(cast, "hello world!!") == 0, "sappend cast");
strcmp(converted, "hello world!!") == 0, "sappend converted" free((void *)cast);
);
} }
static void test_string_buf_cappend() { static void test_string_buf_cappend() {
@ -27,10 +26,9 @@ static void test_string_buf_cappend() {
sput_fail_unless( sput_fail_unless(
string_buf_size(sb) == strlen("hello world!!"), "cappend size" string_buf_size(sb) == strlen("hello world!!"), "cappend size"
); );
const char *converted = string_buf_convert(sb); const char *cast = string_buf_cast(sb);
sput_fail_unless( sput_fail_unless(strcmp(cast, "hello world!!") == 0, "cappend cast");
strcmp(converted, "hello world!!") == 0, "cappend converted" free((void *)cast);
);
} }
static void test_string_buf_nonzero_capacity() { static void test_string_buf_nonzero_capacity() {

View File

@ -22,10 +22,51 @@ static void test_strcmp_ci() {
const char *a1 = "aBcD"; const char *a1 = "aBcD";
const char *a2 = "AbCd"; const char *a2 = "AbCd";
sput_fail_unless(strcmp_ci(a1, a2) == 0, "strcmp_ci == 0"); sput_fail_unless(strcmp_ci(a1, a2) == 0, "strcmp_ci == 0");
const char *b1 = "aBcDe"; const char *b1 = "aBcDe";
const char *b2 = "AbCd"; const char *b2 = "AbCd";
sput_fail_unless(strcmp_ci(b1, b2) > 0, "strcmp_ci > 0"); sput_fail_unless(strcmp_ci(b1, b2) > 0, "strcmp_ci > 0");
sput_fail_unless(strcmp_ci(b2, b1) < 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 */ #endif /* _BOOTSTRAP_TEST_STRING_UTILS */

View File

@ -163,6 +163,46 @@ static void test_validator_valid_type_ci() {
test_validator_teardown(fixture); 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() { static void test_validator_field_prompt_invalid() {
struct TestValidatorFixture *fixture = test_validator_setup( struct TestValidatorFixture *fixture = test_validator_setup(
"{" "{"
@ -183,7 +223,7 @@ static void test_validator_field_prompt_invalid() {
test_validator_teardown(fixture); test_validator_teardown(fixture);
} }
static void test_validator_valid() { static void test_validator_valid_no_required() {
struct TestValidatorFixture *fixture = test_validator_setup( struct TestValidatorFixture *fixture = test_validator_setup(
"{" "{"
" \"key\": {" " \"key\": {"