parent
e987caf64b
commit
383ada8661
19
README.md
19
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
|
||||
|
||||
|
|
|
@ -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 */
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"app": {
|
||||
"type": "line",
|
||||
"prompt": "App Name> "
|
||||
"prompt": "App> "
|
||||
},
|
||||
"module": {
|
||||
"type": "line",
|
||||
"prompt": "Module (optional)> "
|
||||
"required": false,
|
||||
"prompt": "Module> "
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"app": {
|
||||
"type": "line",
|
||||
"prompt": "App Name> "
|
||||
"prompt": "App> "
|
||||
},
|
||||
"module": {
|
||||
"type": "line",
|
||||
"prompt": "Module (optional)> "
|
||||
"required": false,
|
||||
"prompt": "Module> "
|
||||
}
|
||||
}
|
||||
|
|
19
src/config.c
19
src/config.c
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
137
src/evaluator.c
137
src/evaluator.c
|
@ -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;
|
||||
}
|
||||
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);
|
||||
|
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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\": {"
|
||||
|
|
Loading…
Reference in New Issue