diff --git a/README.md b/README.md index d948083..24f761e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/include/error.h b/include/error.h index 61b2111..19b053f 100644 --- a/include/error.h +++ b/include/error.h @@ -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. diff --git a/include/validator.h b/include/validator.h index 0f6d061..6189853 100644 --- a/include/validator.h +++ b/include/validator.h @@ -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 */ diff --git a/main.c b/main.c index bcbc83d..6d61f0a 100644 --- a/main.c +++ b/main.c @@ -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; } diff --git a/src/config.c b/src/config.c index aceecd1..2b6157d 100644 --- a/src/config.c +++ b/src/config.c @@ -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; } diff --git a/src/evaluator.c b/src/evaluator.c index efed46c..d2a139c 100644 --- a/src/evaluator.c +++ b/src/evaluator.c @@ -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; diff --git a/src/parser.c b/src/parser.c index 0b32409..ef86e8f 100644 --- a/src/parser.c +++ b/src/parser.c @@ -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." ); } diff --git a/src/validator.c b/src/validator.c index 5d8f65a..45292b8 100644 --- a/src/validator.c +++ b/src/validator.c @@ -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; } diff --git a/test/test_validator.h b/test/test_validator.h index 02e21ac..345b29e 100644 --- a/test/test_validator.h +++ b/test/test_validator.h @@ -1,14 +1,18 @@ #ifndef _BOOTSTRAP_TEST_VALIDATOR #define _BOOTSTRAP_TEST_VALIDATOR +#include + #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);