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`
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

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
/// 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.

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]);
/**
@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.

View File

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

View File

@ -5,6 +5,8 @@
#ifndef _BOOTSTRAP_VALIDATOR_H
#define _BOOTSTRAP_VALIDATOR_H
#include <stdbool.h>
#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;

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@
#include <string.h>
#include <sys/stat.h>
#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;

View File

@ -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;
}

View File

@ -7,10 +7,13 @@
#include <string.h>
#include <sys/stat.h>
#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;
} else {
}
} 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, "=");
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."
);
print_header(config);
if ((*error = push_fields(config, fields, &env_buf))) {
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 *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);

View File

@ -5,6 +5,7 @@
#include <stdbool.h>
#include <stdio.h>
#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."
);
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -2,6 +2,7 @@
#include <ctype.h>
#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."
);
}

View File

@ -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();

View File

@ -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() {

View File

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

View File

@ -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\": {"