Add consistent error output.

pull/9/head
Joshua Potter 2023-11-26 06:30:44 -07:00
parent df65c8bcac
commit 80b1f4ed49
9 changed files with 161 additions and 59 deletions

View File

@ -2,10 +2,6 @@
CLI utility for defining custom project initialization scripts.
TODO:
- [ ] Add evaluator tests.
- [ ] Color output to console.
## Overview
`bootstrap` is a tool for quickly defining your own init-like scripts. If you

View File

@ -95,7 +95,7 @@ Take the `__VA_ARGS__` list and append a list of decreasing numbers
__VA_ARGS__, \
0x1F, 0x1E, 0x1D, 0x1C, 0x1B, 0x1A, 0x19, 0x18, \
0x17, 0x16, 0x15, 0x14, 0x13, 0x12, 0x11, 0x10, \
0x0E, 0x0F, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, \
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, \
0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00)
#define ALEN0( \
@ -121,13 +121,20 @@ It is the responsibility of the caller to free the @ref Error instance.
#define ERROR_NEW(code, ...) \
ERROR_NEW0(code, ALEN(__VA_ARGS__), __VA_ARGS__)
#define ERROR_NEW0(code, nargs, ...) \
priv_error_new( \
(code), (nargs + 1), (const char * [nargs + 1]){__VA_ARGS__, "\n"} \
)
#define ERROR_NEW0(code, nargs, ...) \
priv_error_new((code), (nargs), (const char *[nargs]){__VA_ARGS__})
// 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

@ -6,6 +6,7 @@
#define _BOOTSTRAP_VALIDATOR_H
#include "cJSON.h"
#include "config.h"
#include "dyn_array.h"
#include "error.h"
@ -52,6 +53,8 @@ struct Field {
/**
@brief Verify the `spec.json` file is formatted correctly.
@param config
A reference to the parameters describing the desired spec.
@param parsed
A possible null pointer to the parsed `spec.json` file. If null, this method
simply sets *fields to a null pointer.
@ -62,7 +65,9 @@ struct Field {
A null pointer if no error occurs. Otherwise an @ref Error pointer.
*/
struct Error *validate_spec_json(
const cJSON *const parsed, struct DynArray **fields
const struct Config *const config,
const cJSON *const parsed,
struct DynArray **fields
);
#endif /* _BOOTSTRAP_VALIDATOR_H */

2
main.c
View File

@ -33,7 +33,7 @@ static int run(const char *root_dir, const char *target) {
}
struct DynArray *prompts = 0;
if ((error = validate_spec_json(parsed, &prompts))) {
if ((error = validate_spec_json(config, parsed, &prompts))) {
fprintf(stderr, "%s", error->message);
goto cleanup_parsed;
}

View File

@ -17,11 +17,19 @@ struct Error *config_new(
assert(target);
if (cwd == 0) {
return ERROR_NEW(ERROR_CONFIG_ENV_CWD_INVALID, "Could not retrieve $CWD.");
return ERROR_NEW(
ERROR_CONFIG_ENV_CWD_INVALID,
ANSI_RED("ERROR"),
": Could not retrieve ",
ANSI_CYAN("CWD"),
"."
);
}
if (root_dir == 0) {
return ERROR_NEW(
ERROR_CONFIG_ENV_ROOT_DIR_INVALID, "No specified root directory."
ERROR_CONFIG_ENV_ROOT_DIR_INVALID,
ANSI_RED("ERROR"),
": Could not find root directory."
);
}
@ -35,18 +43,30 @@ struct Error *config_new(
if (stat_res == -1) {
if (errno == ENOENT) {
error = ERROR_NEW(
ERROR_CONFIG_TARGET_NOT_FOUND, "Spec ", filepath, " not found."
ERROR_CONFIG_TARGET_NOT_FOUND,
ANSI_RED("ERROR"),
": Could not find ",
ANSI_BLUE(target),
" spec."
);
} else {
error = ERROR_NEW(
ERROR_CONFIG_TARGET_INVALID, "Spec ", filepath, " is invalid."
ERROR_CONFIG_TARGET_INVALID,
ANSI_RED("ERROR"),
": ",
ANSI_BLUE(target),
" is invalid."
);
}
goto cleanup;
}
if (!S_ISDIR(sb.st_mode)) {
error = ERROR_NEW(
ERROR_CONFIG_TARGET_NOT_DIR, "Spec ", filepath, " is not a directory."
ERROR_CONFIG_TARGET_NOT_DIR,
ANSI_RED("ERROR"),
": ",
ANSI_CYAN(filepath),
" is not a directory."
);
goto cleanup;
}

View File

@ -23,17 +23,20 @@ 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,
"Could not find ",
config->target,
"/runner"
ANSI_RED("NOT_FOUND"),
": Could not find ",
ANSI_BLUE(config->target, "/runner"),
"."
);
}
if (!(sb.st_mode & S_IXUSR)) {
return ERROR_NEW(
ERROR_EVALUATOR_RUNNER_NOT_EXEC,
config->target,
"/runner is not executable."
ANSI_RED("ERROR"),
": ",
ANSI_BLUE(config->target, "/runner"),
" is not executable."
);
}
@ -42,20 +45,21 @@ static struct Error *find_run_exec(const struct Config *const config) {
static const char *prompt_field(struct Field *field) {
assert(field);
printf("%s", field->prompt);
char *response = calloc(1, 1024);
switch (field->type) {
case FT_TEXT:
printf("%s", field->prompt);
// TODO: Probably want this buffer size to be a bit more dynamic.
char *input = calloc(1, 1024);
if (fgets(input, 1024, stdin)) {
size_t len = strlen(input);
if (len > 0 && input[len - 1] == '\n') {
input[len - 1] = '\0';
if (fgets(response, 1024, stdin)) {
size_t len = strlen(response);
if (len > 0 && response[len - 1] == '\n') {
response[len - 1] = '\0';
}
return input;
return response;
} else {
free(input);
free(response);
return 0;
}
}
@ -92,7 +96,9 @@ int evaluate_runner(
const char *response = prompt_field(field);
if (!response) {
*error = ERROR_NEW(
ERROR_EVALUATOR_RESPONSE_INVALID, "Could not read in response."
ERROR_EVALUATOR_RESPONSE_INVALID,
ANSI_RED("ERROR"),
": Could not read response."
);
string_buf_free(env_buf);
return EXIT_FAILURE;

View File

@ -19,7 +19,11 @@ static struct Error *find_spec_json(
*handle = fopen(filepath, "r");
if (!*handle && errno != ENOENT) {
error = ERROR_NEW(
ERROR_PARSER_SPEC_JSON_INVALID, config->target, "/spec.json is invalid."
ERROR_PARSER_SPEC_JSON_INVALID,
ANSI_RED("ERROR"),
": ",
ANSI_BLUE(config->target, "/spec.json"),
" is invalid."
);
}
@ -60,7 +64,10 @@ struct Error *parse_spec_json(
if (!*parsed) {
return ERROR_NEW(
ERROR_PARSER_SPEC_JSON_INVALID_SYNTAX,
"The spec.json file contains invalid JSON."
ANSI_RED("ERROR"),
": ",
ANSI_BLUE(config->target, "/spec.json"),
" contains invalid JSON."
);
}

View File

@ -4,27 +4,44 @@
#include "string_utils.h"
static struct Error *read_field(const cJSON *const field, struct Field **out) {
static struct Error *read_field(
const struct Config *const config,
const cJSON *const field,
struct Field **out
) {
if (!cJSON_IsObject(field)) {
return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NOT_OBJECT,
"Field \"",
field->string,
"\" is not a JSON object."
ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" is not a JSON object."
);
}
if (isdigit(field->string[0])) {
return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NAME_INVALID,
"Field names may not begin with a digit."
ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" may not begin with a digit."
);
} else {
for (const char *c = field->string; *c; ++c) {
if (*c != '_' && !isalnum(*c)) {
return ERROR_NEW(
ERROR_VALIDATOR_FIELD_NAME_INVALID,
"Field names must consist of alphanumeric characters or underscores."
ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" must consist of only alphanumeric characters and underscores."
);
}
}
@ -38,9 +55,14 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
if (!cJSON_IsString(type)) {
error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_INVALID,
"Field \"",
field->string,
"\" has non-string \"type\"."
ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" has non-string ",
ANSI_PURPLE("type"),
"."
);
goto cleanup;
}
@ -50,9 +72,14 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
} else {
error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN,
"Field \"",
field->string,
"\" has unknown \"type\"."
ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" has unknown ",
ANSI_PURPLE("type"),
"."
);
goto cleanup;
}
@ -63,9 +90,14 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
} else {
error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
"Field \"",
field->string,
"\" has non-string \"prompt\"."
ANSI_RED("ERROR"),
": Field ",
ANSI_PURPLE(field->string),
" in ",
ANSI_BLUE(config->target, "/spec.json"),
" has non-string ",
ANSI_PURPLE("prompt"),
"."
);
goto cleanup;
}
@ -78,7 +110,9 @@ cleanup:
}
struct Error *validate_spec_json(
const cJSON *const parsed, struct DynArray **fields
const struct Config *const config,
const cJSON *const parsed,
struct DynArray **fields
) {
*fields = 0;
@ -90,7 +124,10 @@ struct Error *validate_spec_json(
if (!cJSON_IsObject(parsed)) {
return ERROR_NEW(
ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT,
"Top-level JSON value in spec.json is not an object."
ANSI_RED("ERROR"),
": Top-level JSON value in ",
ANSI_BLUE(config->target, "/spec.json"),
" is not an object."
);
}
@ -102,7 +139,7 @@ struct Error *validate_spec_json(
cJSON *child = parsed->child;
while (child) {
struct Field *field = 0;
error = read_field(child, &field);
error = read_field(config, child, &field);
if (error) {
goto cleanup;
}

View File

@ -1,14 +1,18 @@
#ifndef _BOOTSTRAP_TEST_VALIDATOR
#define _BOOTSTRAP_TEST_VALIDATOR
#include <unistd.h>
#include "dyn_array.h"
#include "sput.h"
#include "string_utils.h"
#include "validator.h"
struct TestValidatorFixture {
const char *json;
struct DynArray *prompts;
cJSON *parsed;
struct Config config;
};
static struct TestValidatorFixture *test_validator_setup(const char *json) {
@ -17,6 +21,15 @@ static struct TestValidatorFixture *test_validator_setup(const char *json) {
fixture->json = json;
fixture->prompts = 0;
fixture->parsed = cJSON_Parse(json);
char *cwd = getcwd(0, 0);
const char *segments[] = {cwd, "test", "specs"};
char *root_dir = join(sizeof(segments) / sizeof(char *), segments, '/');
fixture->config.cwd = cwd;
fixture->config.root_dir = root_dir;
fixture->config.target = "minimal_spec_json";
return fixture;
}
@ -24,13 +37,16 @@ static void test_validator_teardown(struct TestValidatorFixture *fixture) {
if (fixture->parsed) {
cJSON_Delete(fixture->parsed);
}
free((void *)fixture->config.cwd);
free((void *)fixture->config.root_dir);
free(fixture);
}
static void test_validator_toplevel_not_object() {
struct TestValidatorFixture *fixture = test_validator_setup("[]");
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_TOP_LEVEL_NOT_OBJECT, "top-level not object"
);
@ -43,7 +59,8 @@ static void test_validator_field_not_object() {
struct TestValidatorFixture *fixture =
test_validator_setup("{\"key\": \"$UNKNOWN\"}");
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_NOT_OBJECT, "field not object"
);
@ -61,7 +78,8 @@ static void test_validator_field_name_leading_digit() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_NAME_INVALID,
"field name leading digit"
@ -80,7 +98,8 @@ static void test_validator_field_name_non_alnum() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_NAME_INVALID, "field name non alnum"
);
@ -98,7 +117,8 @@ static void test_validator_field_type_invalid() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_TYPE_INVALID, "field type invalid"
);
@ -116,7 +136,8 @@ static void test_validator_field_type_unknown() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, "field type unknown"
);
@ -135,7 +156,8 @@ static void test_validator_valid_type_ci() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(error == 0, "valid");
test_validator_teardown(fixture);
@ -151,7 +173,8 @@ static void test_validator_field_prompt_invalid() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(
error->code == ERROR_VALIDATOR_FIELD_PROMPT_INVALID, "field prompt invalid"
);
@ -170,7 +193,8 @@ static void test_validator_valid() {
"}"
);
struct Error *error = validate_spec_json(fixture->parsed, &fixture->prompts);
struct Error *error =
validate_spec_json(&fixture->config, fixture->parsed, &fixture->prompts);
sput_fail_unless(error == 0, "valid");
test_validator_teardown(fixture);