diff --git a/fall-2025/sen-210/Executables/JobWizard.zip b/fall-2025/sen-210/Executables/JobWizard.zip new file mode 100644 index 0000000..055671a Binary files /dev/null and b/fall-2025/sen-210/Executables/JobWizard.zip differ diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/compile_examples.sh b/fall-2025/sen-210/Executables/JobWizard/console_example/compile_examples.sh new file mode 100644 index 0000000..95f681e --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/console_example/compile_examples.sh @@ -0,0 +1,2 @@ +gcc -o console_example console_example.c +gcc -o console_json -DJSON_SKIP_WHITESPACE console_json.c json.c diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/console_example b/fall-2025/sen-210/Executables/JobWizard/console_example/console_example new file mode 100755 index 0000000..5bdff72 Binary files /dev/null and b/fall-2025/sen-210/Executables/JobWizard/console_example/console_example differ diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/console_example.c b/fall-2025/sen-210/Executables/JobWizard/console_example/console_example.c new file mode 100644 index 0000000..41c0182 --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/console_example/console_example.c @@ -0,0 +1,65 @@ +/* Example program showing how to invoke job_wizard + * from a simple console program in order to execute a task, + * then capture the output and print (without parsing). + * + * Created by Sally Goldin for SEN-210 on 15 September 2025 + */ +#include +#include +#include + +// Open the redirect file and display the contents +void displayOutput(char* redirectFilename) +{ + FILE* pF = NULL; + char* textbuffer = NULL; + + pF = fopen(redirectFilename,"r"); + if (pF != NULL) + { + // get the size of the file + fseek(pF, 0L, SEEK_END); + int size = ftell(pF); + rewind(pF); + textbuffer = calloc(size + 2, sizeof(char)); // +2 for terminating 0 + if (textbuffer == NULL) + { + printf("Error allocating space to store results\n"); + return; + } + if (fread(textbuffer,sizeof(char),size,pF) != size) + { + printf("Error reading data from result file\n"); + return; + } + printf("RESULTS\n"); + printf(textbuffer); + free(textbuffer); + } +} + +/* main program executes a search with no arguments */ +int main(int argc, char* argv) +{ + char* userEmail = "sally@cmkl.ac.th"; + char* outputFile = "output.txt"; + char jobwizardCmd[2048]; + int returnCode; + + // Create the command to execute + // In your real console-based UI, you probably want different functions + // that know about the arguments for different job_wizard commands + sprintf(jobwizardCmd,"./job_wizard -task search -email %s > %s 2>&1",userEmail,outputFile); + // note that 2>&1 redirects both standard output and standard error to the file output.txt + returnCode = system(jobwizardCmd); + if (returnCode != 0) + { + printf("Error %d executing job_wizard command\n"); + printf("Command: |%s|\n",jobwizardCmd); + } + else + { + printf("Success!\n"); + displayOutput(outputFile); + } +} \ No newline at end of file diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/console_json b/fall-2025/sen-210/Executables/JobWizard/console_example/console_json new file mode 100755 index 0000000..ccaceb1 Binary files /dev/null and b/fall-2025/sen-210/Executables/JobWizard/console_example/console_json differ diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/console_json.c b/fall-2025/sen-210/Executables/JobWizard/console_example/console_json.c new file mode 100644 index 0000000..b8f9eb0 --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/console_example/console_json.c @@ -0,0 +1,201 @@ +/* console_json.c + * Example program showing how to invoke job_wizard + * from a simple console program in order to execute a task, + * then capture the output and parse as JSON. + * + * Uses https://github.com/whyisitworking/C-Simple-JSON-Parser + * + * Created by Sally Goldin for SEN-210 on 15 September 2025 + */ +#include +#include +#include + +#include "json.h" + +// structure to hold one search result, a job summary +typedef struct +{ + char job_id[8]; + char title[64]; + int is_open; // boolean + char date_posted[32]; +} JOB_SUMMARY_T; + +// Open the redirect file, reads and returns the content +// as a null terminated text string. Note that the returned +// string must be freed by the caller when no longer needed +char* readOutput(char* redirectFilename) +{ + FILE* pF = NULL; + char* textbuffer = NULL; + + pF = fopen(redirectFilename,"r"); + if (pF != NULL) + { + // get the size of the file + fseek(pF, 0L, SEEK_END); + int size = ftell(pF); + rewind(pF); + textbuffer = calloc(size+2, sizeof(char)); // +2 for 0 term + if (textbuffer == NULL) + { + printf("Error allocating space to store results\n"); + return NULL; + } + if (fread(textbuffer,sizeof(char),size,pF) != size) + { + printf("Error reading data from result file\n"); + return NULL; + } + } + return (textbuffer); +} + +/* parse the passed string and return the generic structure (json_element) + * build by C-Simple-JSON-Parser. + * Also returns a status via the integer pointer - 0 if okay, else -1 + */ +typed(json_element) parseJsonString(char* rawJsonText, int* pStatus) +{ + // based on example in the C-Simple-JSON-Parser repo + typed(json_element) element; + *pStatus = 0; // assume it will work + + result(json_element) element_result = json_parse(rawJsonText); + if (result_is_err(json_element)(&element_result)) + { + typed(json_error) error = result_unwrap_err(json_element)(&element_result); + fprintf(stderr, "Error parsing JSON: %s\n", json_error_to_string(error)); + *pStatus = -1; + } + else + { + element = result_unwrap(json_element)(&element_result); + } + return element; +} + +/* Attempts to parse the string passed as rawJsonText into + * an array of structures that represent job summaries. + * This is a two step process. The first, which parses the JSON, + * will be the same for all job_wizard results. The second extracts + * job summary information from the generic structures produced + * by the first. + * Returns 0 for success, -1 for error + * If successful, also allocates and returns an array of results. + * This array must be freed by the caller. + * Also sets the value of pJobCount + */ +int parseSearchResults(char* rawJsonText, JOB_SUMMARY_T** allResults,int *pJobCount) +{ + int status = 0; + int i,j; + JOB_SUMMARY_T * resultArray = NULL; + + typed(json_element) element = parseJsonString(rawJsonText,&status); + if (status < 0) + { + return status; // do we need to free the element? + } + // we expect an array of jobs + typed(json_array) *arr = element.value.as_array; + *pJobCount = arr->count; + printf("Found %d jobs\n",*pJobCount); + resultArray = (JOB_SUMMARY_T*) calloc(*pJobCount,sizeof(JOB_SUMMARY_T)); + if (resultArray == NULL) + { + printf("Error allocating job summary structures\n"); + status = -1; + return status; + } + *allResults = resultArray; + for (j=0; j < *pJobCount; j++) + { + typed(json_element) element = arr->elements[j]; + typed(json_object) *obj = element.value.as_object; + for (i = 0; i < obj->count; i++) + { + typed(json_entry) entry = *(obj->entries[i]); + typed(json_string) key = entry.key; + typed(json_element_value) value = entry.element.value; + if (strcmp(key,"job_id") == 0) + { + strcpy(resultArray[j].job_id,value.as_string); + } + else if (strcmp(key,"title") == 0) + { + strcpy(resultArray[j].title,value.as_string); + } + else if (strcmp(key,"is_open") == 0) + { + if (value.as_boolean) + resultArray[j].is_open = 1; + else + resultArray[j].is_open = 0; + } + else if (strcmp(key,"date_posted") == 0) + { + strcpy(resultArray[j].date_posted,value.as_string); + } + else + { + printf("Unrecognized object key %s\n",key); + } + } + } + json_free(&element); + + return status; +} + +/* Display the contents of a job structure as a line of text + */ +void printJobSummary(JOB_SUMMARY_T job) +{ + printf("%6s %32s %4s %12s\n", + job.job_id,job.title,job.is_open? "t" : "f", job.date_posted); +} + +/* main program executes a search with no arguments */ +int main(int argc, char* argv) +{ + char* userEmail = "sally@cmkl.ac.th"; + char* outputFile = "output.txt"; + char jobwizardCmd[2048]; + char* resultText = NULL; // holds results, must be freed after use + int returnCode; + int jobCount; + int j; + JOB_SUMMARY_T * searchResults = NULL; // array of structs allocated for parsed JSON + + // Create the command to execute + // In your real console-based UI, you probably want different functions + // that know about the arguments for different job_wizard commands + sprintf(jobwizardCmd,"./job_wizard -task search -email %s > %s 2>&1",userEmail,outputFile); + // note that 2>&1 redirects both standard output and standard error to the file output.txt + returnCode = system(jobwizardCmd); + if (returnCode != 0) + { + printf("Error %d executing job_wizard command\n"); + printf("Command: |%s|\n",jobwizardCmd); + } + else + { + printf("Successfully ran job_wizard!\n"); + resultText = readOutput(outputFile); + if (resultText != NULL) + { + returnCode = parseSearchResults(resultText,&searchResults,&jobCount); + if (returnCode != 0) + printf("Error parsing JSON\n"); + if (searchResults != NULL) + { + for (j = 0; j < jobCount; j++) + printJobSummary(searchResults[j]); + free(searchResults); + } + free(resultText); + } + } +} diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/json.c b/fall-2025/sen-210/Executables/JobWizard/console_example/json.c new file mode 100644 index 0000000..f47f52c --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/console_example/json.c @@ -0,0 +1,1104 @@ +#include "json.h" + +#include +#include +#include +#include +#include +#include + +/** + * @brief Determines whether a character `ch` is whitespace + */ +#define is_whitespace(ch) (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t') + +#ifdef JSON_SKIP_WHITESPACE +void json_skip_whitespace(typed(json_string) * str_ptr) { + while (is_whitespace(**str_ptr)) + (*str_ptr)++; +} +#else +#define json_skip_whitespace(arg) +#endif + +#ifdef JSON_DEBUG +#define log(str, ...) printf(str "\n", ##__VA_ARGS__) +void json_debug_print(typed(json_string) str, typed(size) len) { + for (size_t i = 0; i < len; i++) { + if (str[i] == '\0') + break; + + putchar(str[i]); + } + printf("\n"); +} +#else +#define log(str, ...) +#endif + +#define define_result_type(name) \ + result(name) result_ok(name)(typed(name) value) { \ + result(name) retval = { \ + .is_ok = true, \ + .inner = \ + { \ + .value = value, \ + }, \ + }; \ + return retval; \ + } \ + result(name) result_err(name)(typed(json_error) err) { \ + result(name) retval = { \ + .is_ok = false, \ + .inner = \ + { \ + .err = err, \ + }, \ + }; \ + return retval; \ + } \ + typed(json_boolean) result_is_ok(name)(result(name) * result) { \ + return result->is_ok; \ + } \ + typed(json_boolean) result_is_err(name)(result(name) * result) { \ + return !result->is_ok; \ + } \ + typed(name) result_unwrap(name)(result(name) * result) { \ + return result->inner.value; \ + } \ + typed(json_error) result_unwrap_err(name)(result(name) * result) { \ + return result->inner.err; \ + } + +/** + * @brief Allocate `count` number of items of `type` in memory + * and return the pointer to the newly allocated memory + */ +#define allocN(type, count) (type *)malloc((count) * sizeof(type)) + +/** + * @brief Allocate an item of `type` in memory and return the + * pointer to the newly allocated memory + */ +#define alloc(type) allocN(type, 1) + +/** + * @brief Re-allocate `count` number of items of `type` in memory + * and return the pointer to the newly allocated memory + */ +#define reallocN(ptr, type, count) (type *)realloc(ptr, (count) * sizeof(type)) + +/** + * @brief Parses a JSON element {json_element_t} and moves the string + * pointer to the end of the parsed element + */ +static result(json_entry) json_parse_entry(typed(json_string) *); + +/** + * @brief Guesses the element type at the start of a string + */ +static result(json_element_type) json_guess_element_type(typed(json_string)); + +/** + * @brief Whether a token represents a string. Like '"' + */ +static bool json_is_string(char); + +/** + * @brief Whether a token represents a number. Like '0' + */ +static bool json_is_number(char); + +/** + * @brief Whether a token represents a object. Like '"' + */ +static bool json_is_object(char); + +/** + * @brief Whether a token represents a array. Like '[' + */ +static bool json_is_array(char); + +/** + * @brief Whether a token represents a boolean. Like 't' + */ +static bool json_is_boolean(char); + +/** + * @brief Whether a token represents a null. Like 'n' + */ +static bool json_is_null(char); + +/** + * @brief Parses a JSON element value {json_element_value_t} based + * on the `type` parameter passed and moves the string pointer + * to end of the parsed element + */ +static result(json_element_value) + json_parse_element_value(typed(json_string) *, typed(json_element_type)); + +/** + * @brief Parses a `String` {json_string_t} and moves the string + * pointer to the end of the parsed string + */ +static result(json_element_value) json_parse_string(typed(json_string) *); + +/** + * @brief Parses a `Number` {json_number_t} and moves the string + * pointer to the end of the parsed number + */ +static result(json_element_value) json_parse_number(typed(json_string) *); + +/** + * @brief Parses a `Object` {json_object_t} and moves the string + * pointer to the end of the parsed object + */ +static result(json_element_value) json_parse_object(typed(json_string) *); + +static typed(uint64) json_key_hash(typed(json_string)); + +/** + * @brief Parses a `Array` {json_array_t} and moves the string + * pointer to the end of the parsed array + */ +static result(json_element_value) json_parse_array(typed(json_string) *); + +/** + * @brief Parses a `Boolean` {json_boolean_t} and moves the string + * pointer to the end of the parsed boolean + */ +static result(json_element_value) json_parse_boolean(typed(json_string) *); + +/** + * @brief Skips a Key-Value pair + * + * @return true If a valid entry is skipped + * @return false If entry was invalid (still skips) + */ +static bool json_skip_entry(typed(json_string) *); + +/** + * @brief Skips an element value + * + * @return true If a valid element is skipped + * @return false If element was invalid (still skips) + */ +static bool json_skip_element_value(typed(json_string) *, + typed(json_element_type)); + +/** + * @brief Skips a string value + * + * @return true If a valid string is skipped + * @return false If string was invalid (still skips) + */ +static bool json_skip_string(typed(json_string) *); + +/** + * @brief Skips a number value + * + * @return true If a valid number is skipped + * @return false If number was invalid (still skips) + */ +static bool json_skip_number(typed(json_string) *); + +/** + * @brief Skips an object value + * + * @return true If a valid object is skipped + * @return false If object was invalid (still skips) + */ +static bool json_skip_object(typed(json_string) *); + +/** + * @brief Skips an array value + * + * @return true If a valid array is skipped + * @return false If array was invalid (still skips) + */ +static bool json_skip_array(typed(json_string) *); + +/** + * @brief Skips a boolean value + * + * @return true If a valid boolean is skipped + * @return false If boolean was invalid (still skips) + */ +static bool json_skip_boolean(typed(json_string) *); + +/** + * @brief Moves a JSON string pointer beyond any whitespace + */ +// static void json_skip_whitespace_actual(typed(json_string) *); + +/** + * @brief Moves a JSON string pointer beyond `null` literal + * + */ +static void json_skip_null(typed(json_string) *); + +/** + * @brief Prints a JSON element {json_element_t} type + */ +static void json_print_element(typed(json_element) *, int, int); + +/** + * @brief Prints a `String` {json_string_t} type + */ +static void json_print_string(typed(json_string)); + +/** + * @brief Prints a `Number` {json_number_t} type + */ +static void json_print_number(typed(json_number)); + +/** + * @brief Prints an `Object` {json_object_t} type + */ +static void json_print_object(typed(json_object) *, int, int); + +/** + * @brief Prints an `Array` {json_array_t} type + */ +static void json_print_array(typed(json_array) *, int, int); + +/** + * @brief Prints a `Boolean` {json_boolean_t} type + */ +static void json_print_boolean(typed(json_boolean)); + +/** + * @brief Frees a `String` (json_string_t) from memory + */ +static void json_free_string(typed(json_string)); + +/** + * @brief Frees an `Object` (json_object_t) from memory + */ +static void json_free_object(typed(json_object) *); + +/** + * @brief Frees an `Array` (json_array_t) from memory + */ +static void json_free_array(typed(json_array) *); + +/** + * @brief Utility function to convert an escaped string to a formatted string + */ +static result(json_string) + json_unescape_string(typed(json_string), typed(size)); + +/** + * @brief Offset to the last `"` of a JSON string + */ +static typed(size) json_string_len(typed(json_string)); + +result(json_element) json_parse(typed(json_string) json_str) { + if (json_str == NULL) { + return result_err(json_element)(JSON_ERROR_EMPTY); + } + + typed(size) len = strlen(json_str); + if (len == 0) { + return result_err(json_element)(JSON_ERROR_EMPTY); + } + + result_try(json_element, json_element_type, type, + json_guess_element_type(json_str)); + result_try(json_element, json_element_value, value, + json_parse_element_value(&json_str, type)); + + const typed(json_element) element = { + .type = type, + .value = value, + }; + + return result_ok(json_element)(element); +} + +result(json_entry) json_parse_entry(typed(json_string) * str_ptr) { + result_try(json_entry, json_element_value, key, json_parse_string(str_ptr)); + json_skip_whitespace(str_ptr); + + // Skip the ':' delimiter + (*str_ptr)++; + + json_skip_whitespace(str_ptr); + + result(json_element_type) type_result = json_guess_element_type(*str_ptr); + if (result_is_err(json_element_type)(&type_result)) { + free((void *)key.as_string); + return result_map_err(json_entry, json_element_type, &type_result); + } + typed(json_element_type) type = + result_unwrap(json_element_type)(&type_result); + + result(json_element_value) value_result = + json_parse_element_value(str_ptr, type); + if (result_is_err(json_element_value)(&value_result)) { + free((void *)key.as_string); + return result_map_err(json_entry, json_element_value, &value_result); + } + typed(json_element_value) value = + result_unwrap(json_element_value)(&value_result); + + typed(json_entry) entry = { + .key = key.as_string, + .element = + { + .type = type, + .value = value, + }, + }; + + return result_ok(json_entry)(entry); +} + +result(json_element_type) json_guess_element_type(typed(json_string) str) { + const char ch = *str; + typed(json_element_type) type; + + if (json_is_string(ch)) + type = JSON_ELEMENT_TYPE_STRING; + else if (json_is_object(ch)) + type = JSON_ELEMENT_TYPE_OBJECT; + else if (json_is_array(ch)) + type = JSON_ELEMENT_TYPE_ARRAY; + else if (json_is_null(ch)) + type = JSON_ELEMENT_TYPE_NULL; + else if (json_is_number(ch)) + type = JSON_ELEMENT_TYPE_NUMBER; + else if (json_is_boolean(ch)) + type = JSON_ELEMENT_TYPE_BOOLEAN; + else + return result_err(json_element_type)(JSON_ERROR_INVALID_TYPE); + + return result_ok(json_element_type)(type); +} + +bool json_is_string(char ch) { return ch == '"'; } + +bool json_is_number(char ch) { + return (ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == '.' || + ch == 'e' || ch == 'E'; +} + +bool json_is_object(char ch) { return ch == '{'; } + +bool json_is_array(char ch) { return ch == '['; } + +bool json_is_boolean(char ch) { return ch == 't' || ch == 'f'; } + +bool json_is_null(char ch) { return ch == 'n'; } + +result(json_element_value) + json_parse_element_value(typed(json_string) * str_ptr, + typed(json_element_type) type) { + switch (type) { + case JSON_ELEMENT_TYPE_STRING: + return json_parse_string(str_ptr); + case JSON_ELEMENT_TYPE_NUMBER: + return json_parse_number(str_ptr); + case JSON_ELEMENT_TYPE_OBJECT: + return json_parse_object(str_ptr); + case JSON_ELEMENT_TYPE_ARRAY: + return json_parse_array(str_ptr); + case JSON_ELEMENT_TYPE_BOOLEAN: + return json_parse_boolean(str_ptr); + case JSON_ELEMENT_TYPE_NULL: + json_skip_null(str_ptr); + return result_err(json_element_value)(JSON_ERROR_EMPTY); + default: + return result_err(json_element_value)(JSON_ERROR_INVALID_TYPE); + } +} + +result(json_element_value) json_parse_string(typed(json_string) * str_ptr) { + // Skip the first '"' character + (*str_ptr)++; + + typed(size) len = json_string_len(*str_ptr); + if (len == 0) { + // Skip the end quote + (*str_ptr)++; + return result_err(json_element_value)(JSON_ERROR_EMPTY); + } + + result_try(json_element_value, json_string, output, + json_unescape_string(*str_ptr, len)); + + // Skip to beyond the string + (*str_ptr) += len + 1; + + typed(json_element_value) retval = {0}; + retval.as_string = output; + + return result_ok(json_element_value)(retval); +} + +result(json_element_value) json_parse_number(typed(json_string) * str_ptr) { + typed(json_string) temp_str = *str_ptr; + bool has_decimal = false; + + while (json_is_number(*temp_str)) { + if (*temp_str == '.') { + has_decimal = true; + } + + temp_str++; + } + + typed(json_number) number = {0}; + typed(json_number_value) val = {0}; + + if (has_decimal) { + errno = 0; + + val.as_double = strtod(*str_ptr, (char **)str_ptr); + + number.type = JSON_NUMBER_TYPE_DOUBLE; + number.value = val; + + if (errno == EINVAL || errno == ERANGE) + return result_err(json_element_value)(JSON_ERROR_INVALID_VALUE); + } else { + errno = 0; + + val.as_long = strtol(*str_ptr, (char **)str_ptr, 10); + + number.type = JSON_NUMBER_TYPE_LONG; + number.value = val; + + if (errno == EINVAL || errno == ERANGE) + return result_err(json_element_value)(JSON_ERROR_INVALID_VALUE); + } + + typed(json_element_value) retval = {0}; + retval.as_number = number; + + return result_ok(json_element_value)(retval); +} + +result(json_element_value) json_parse_object(typed(json_string) * str_ptr) { + typed(json_string) temp_str = *str_ptr; + + // ******* First find the number of valid entries ******* + // Skip the first '{' character + temp_str++; + + json_skip_whitespace(&temp_str); + + if (*temp_str == '}') { + // Skip the end '}' in the actual pointer + (*str_ptr) = temp_str + 1; + return result_err(json_element_value)(JSON_ERROR_EMPTY); + } + + typed(size) count = 0; + + while (*temp_str != '\0') { + // Skip any accidental whitespace + json_skip_whitespace(&temp_str); + + // If the entry could be skipped + if (json_skip_entry(&temp_str)) { + count++; + } + + // Skip any accidental whitespace + json_skip_whitespace(&temp_str); + + if (*temp_str == '}') + break; + + // Skip the ',' to move to the next entry + temp_str++; + } + + if (count == 0) + return result_err(json_element_value)(JSON_ERROR_EMPTY); + + // ******* Initialize the hash map ******* + // Now we have a perfectly sized hash map + typed(json_entry) **entries = allocN(typed(json_entry) *, count); + for (size_t i = 0; i < count; i++) + entries[i] = NULL; + + // Skip the first '{' character + (*str_ptr)++; + + json_skip_whitespace(str_ptr); + + while (**str_ptr != '\0') { + // Skip any accidental whitespace + json_skip_whitespace(str_ptr); + result(json_entry) entry_result = json_parse_entry(str_ptr); + + if (result_is_ok(json_entry)(&entry_result)) { + typed(json_entry) entry = result_unwrap(json_entry)(&entry_result); + typed(uint64) bucket = json_key_hash(entry.key) % count; + + // Bucket size is exactly count. So there will be at max + // count misses in the worst case + for (size_t i = 0; i < count; i++) { + if (entries[bucket] == NULL) { + typed(json_entry) *temp_entry = alloc(typed(json_entry)); + memcpy(temp_entry, &entry, sizeof(typed(json_entry))); + entries[bucket] = temp_entry; + break; + } + + bucket = (bucket + 1) % count; + } + } + + // Skip any accidental whitespace + json_skip_whitespace(str_ptr); + + if (**str_ptr == '}') + break; + + // Skip the ',' to move to the next entry + (*str_ptr)++; + } + + // Skip the '}' closing brace + (*str_ptr)++; + + typed(json_object) *object = alloc(typed(json_object)); + object->count = count; + object->entries = entries; + + typed(json_element_value) retval = {0}; + retval.as_object = object; + + return result_ok(json_element_value)(retval); +} + +typed(uint64) json_key_hash(typed(json_string) str) { + typed(uint64) hash = 0; + + while (*str != '\0') + hash += (hash * 31) + *str++; + + return hash; +} + +result(json_element_value) json_parse_array(typed(json_string) * str_ptr) { + // Skip the starting '[' character + (*str_ptr)++; + + json_skip_whitespace(str_ptr); + + // Unfortunately the array is empty + if (**str_ptr == ']') { + // Skip the end ']' + (*str_ptr)++; + return result_err(json_element_value)(JSON_ERROR_EMPTY); + } + + typed(size) count = 0; + typed(json_element) *elements = NULL; + + while (**str_ptr != '\0') { + json_skip_whitespace(str_ptr); + + // Guess the type + result(json_element_type) type_result = json_guess_element_type(*str_ptr); + if (result_is_ok(json_element_type)(&type_result)) { + typed(json_element_type) type = + result_unwrap(json_element_type)(&type_result); + + // Parse the value based on guessed type + result(json_element_value) value_result = + json_parse_element_value(str_ptr, type); + if (result_is_ok(json_element_value)(&value_result)) { + typed(json_element_value) value = + result_unwrap(json_element_value)(&value_result); + + count++; + elements = reallocN(elements, typed(json_element), count); + elements[count - 1].type = type; + elements[count - 1].value = value; + } + + json_skip_whitespace(str_ptr); + } + + // Reached the end + if (**str_ptr == ']') + break; + + // Skip the ',' + (*str_ptr)++; + } + + // Skip the ']' closing array + (*str_ptr)++; + + if (count == 0) + return result_err(json_element_value)(JSON_ERROR_EMPTY); + + typed(json_array) *array = alloc(typed(json_array)); + array->count = count; + array->elements = elements; + + typed(json_element_value) retval = {0}; + retval.as_array = array; + + return result_ok(json_element_value)(retval); +} + +result(json_element_value) json_parse_boolean(typed(json_string) * str_ptr) { + typed(json_boolean) output; + + switch (**str_ptr) { + case 't': + output = true; + (*str_ptr) += 4; + break; + + case 'f': + output = false; + (*str_ptr) += 5; + break; + } + + typed(json_element_value) retval = {0}; + retval.as_boolean = output; + + return result_ok(json_element_value)(retval); +} + +result(json_element) + json_object_find(typed(json_object) * obj, typed(json_string) key) { + if (key == NULL || strlen(key) == 0) + return result_err(json_element)(JSON_ERROR_INVALID_KEY); + + typed(uint64) bucket = json_key_hash(key) % obj->count; + + // Bucket size is exactly obj->count. So there will be at max + // obj->count misses in the worst case + for (size_t i = 0; i < obj->count; i++) { + typed(json_entry) *entry = obj->entries[bucket]; + if (strcmp(key, entry->key) == 0) + return result_ok(json_element)(entry->element); + + bucket = (bucket + 1) % obj->count; + } + + return result_err(json_element)(JSON_ERROR_INVALID_KEY); +} + +bool json_skip_entry(typed(json_string) * str_ptr) { + json_skip_string(str_ptr); + + json_skip_whitespace(str_ptr); + + // Skip the ':' delimiter + (*str_ptr)++; + + json_skip_whitespace(str_ptr); + + result(json_element_type) type_result = json_guess_element_type(*str_ptr); + if (result_is_err(json_element_type)(&type_result)) + return false; + + typed(json_element_type) type = + result_unwrap(json_element_type)(&type_result); + + return json_skip_element_value(str_ptr, type); +} + +bool json_skip_element_value(typed(json_string) * str_ptr, + typed(json_element_type) type) { + switch (type) { + case JSON_ELEMENT_TYPE_STRING: + return json_skip_string(str_ptr); + case JSON_ELEMENT_TYPE_NUMBER: + return json_skip_number(str_ptr); + case JSON_ELEMENT_TYPE_OBJECT: + return json_skip_object(str_ptr); + case JSON_ELEMENT_TYPE_ARRAY: + return json_skip_array(str_ptr); + case JSON_ELEMENT_TYPE_BOOLEAN: + return json_skip_boolean(str_ptr); + case JSON_ELEMENT_TYPE_NULL: + json_skip_null(str_ptr); + return false; + + default: + return false; + } +} + +bool json_skip_string(typed(json_string) * str_ptr) { + // Skip the initial '"' + (*str_ptr)++; + + // Find the length till the last '"' + typed(size) len = json_string_len(*str_ptr); + + // Skip till the end of the string + (*str_ptr) += len + 1; + + return len > 0; +} + +bool json_skip_number(typed(json_string) * str_ptr) { + while (json_is_number(**str_ptr)) { + (*str_ptr)++; + } + + return true; +} + +bool json_skip_object(typed(json_string) * str_ptr) { + // Skip the first '{' character + (*str_ptr)++; + + json_skip_whitespace(str_ptr); + + if (**str_ptr == '}') { + // Skip the end '}' + (*str_ptr)++; + return false; + } + + while (**str_ptr != '\0') { + // Skip any accidental whitespace + json_skip_whitespace(str_ptr); + + json_skip_entry(str_ptr); + + // Skip any accidental whitespace + json_skip_whitespace(str_ptr); + + if (**str_ptr == '}') + break; + + // Skip the ',' to move to the next entry + (*str_ptr)++; + } + + // Skip the '}' closing brace + (*str_ptr)++; + + return true; +} + +bool json_skip_array(typed(json_string) * str_ptr) { + // Skip the starting '[' character + (*str_ptr)++; + + json_skip_whitespace(str_ptr); + + // Unfortunately the array is empty + if (**str_ptr == ']') { + // Skip the end ']' + (*str_ptr)++; + return false; + } + + while (**str_ptr != '\0') { + json_skip_whitespace(str_ptr); + + // Guess the type + result(json_element_type) type_result = json_guess_element_type(*str_ptr); + if (result_is_ok(json_element_type)(&type_result)) { + typed(json_element_type) type = + result_unwrap(json_element_type)(&type_result); + + // Parse the value based on guessed type + json_skip_element_value(str_ptr, type); + + json_skip_whitespace(str_ptr); + } + + // Reached the end + if (**str_ptr == ']') + break; + + // Skip the ',' + (*str_ptr)++; + } + + // Skip the ']' closing array + (*str_ptr)++; + + return true; +} + +bool json_skip_boolean(typed(json_string) * str_ptr) { + switch (**str_ptr) { + case 't': + (*str_ptr) += 4; + return true; + + case 'f': + (*str_ptr) += 5; + return true; + } + + return false; +} + +void json_skip_null(typed(json_string) * str_ptr) { (*str_ptr) += 4; } + +void json_print(typed(json_element) * element, int indent) { + json_print_element(element, indent, 0); +} + +void json_print_element(typed(json_element) * element, int indent, + int indent_level) { + + switch (element->type) { + case JSON_ELEMENT_TYPE_STRING: + json_print_string(element->value.as_string); + break; + case JSON_ELEMENT_TYPE_NUMBER: + json_print_number(element->value.as_number); + break; + case JSON_ELEMENT_TYPE_OBJECT: + json_print_object(element->value.as_object, indent, indent_level); + break; + case JSON_ELEMENT_TYPE_ARRAY: + json_print_array(element->value.as_array, indent, indent_level); + break; + case JSON_ELEMENT_TYPE_BOOLEAN: + json_print_boolean(element->value.as_boolean); + break; + case JSON_ELEMENT_TYPE_NULL: + break; + // Do nothing + } +} + +void json_print_string(typed(json_string) string) { printf("\"%s\"", string); } + +void json_print_number(typed(json_number) number) { + switch (number.type) { + case JSON_NUMBER_TYPE_DOUBLE: + printf("%f", number.value.as_double); + break; + + case JSON_NUMBER_TYPE_LONG: + printf("%ld", number.value.as_long); + break; + } +} + +void json_print_object(typed(json_object) * object, int indent, + int indent_level) { + printf("{\n"); + + for (size_t i = 0; i < object->count; i++) { + for (int j = 0; j < indent * (indent_level + 1); j++) + printf(" "); + + typed(json_entry) *entry = object->entries[i]; + + json_print_string(entry->key); + printf(": "); + json_print_element(&entry->element, indent, indent_level + 1); + + if (i != object->count - 1) + printf(","); + printf("\n"); + } + + for (int j = 0; j < indent * indent_level; j++) + printf(" "); + printf("}"); +} + +void json_print_array(typed(json_array) * array, int indent, int indent_level) { + printf("[\n"); + + for (size_t i = 0; i < array->count; i++) { + typed(json_element) element = array->elements[i]; + for (int j = 0; j < indent * (indent_level + 1); j++) + printf(" "); + json_print_element(&element, indent, indent_level + 1); + + if (i != array->count - 1) + printf(","); + printf("\n"); + } + + for (int i = 0; i < indent * indent_level; i++) + printf(" "); + printf("]"); +} + +void json_print_boolean(typed(json_boolean) boolean) { + printf("%s", boolean ? "true" : "false"); +} + +void json_free(typed(json_element) * element) { + switch (element->type) { + case JSON_ELEMENT_TYPE_STRING: + json_free_string(element->value.as_string); + break; + + case JSON_ELEMENT_TYPE_OBJECT: + json_free_object(element->value.as_object); + break; + + case JSON_ELEMENT_TYPE_ARRAY: + json_free_array(element->value.as_array); + break; + + case JSON_ELEMENT_TYPE_NUMBER: + case JSON_ELEMENT_TYPE_BOOLEAN: + case JSON_ELEMENT_TYPE_NULL: + // Do nothing + break; + } +} + +void json_free_string(typed(json_string) string) { free((void *)string); } + +void json_free_object(typed(json_object) * object) { + if (object == NULL) + return; + + if (object->count == 0) { + free(object); + return; + } + + for (size_t i = 0; i < object->count; i++) { + typed(json_entry) *entry = object->entries[i]; + + if (entry != NULL) { + free((void *)entry->key); + json_free(&entry->element); + free(entry); + } + } + + free(object->entries); + free(object); +} + +void json_free_array(typed(json_array) * array) { + if (array == NULL) + return; + + if (array->count == 0) { + free(array); + return; + } + + // Recursively free each element in the array + for (size_t i = 0; i < array->count; i++) { + typed(json_element) element = array->elements[i]; + json_free(&element); + } + + // Lastly free + free(array->elements); + free(array); +} + +typed(json_string) json_error_to_string(typed(json_error) error) { + switch (error) { + case JSON_ERROR_EMPTY: + return "Empty"; + case JSON_ERROR_INVALID_KEY: + return "Invalid key"; + case JSON_ERROR_INVALID_TYPE: + return "Invalid type"; + case JSON_ERROR_INVALID_VALUE: + return "Invalid value"; + + default: + return "Unknown error"; + } +} + +typed(size) json_string_len(typed(json_string) str) { + typed(size) len = 0; + + typed(json_string) iter = str; + while (*iter != '\0') { + if (*iter == '\\') + iter += 2; + + if (*iter == '"') { + len = iter - str; + break; + } + + iter++; + } + + return len; +} + +result(json_string) + json_unescape_string(typed(json_string) str, typed(size) len) { + typed(size) count = 0; + typed(json_string) iter = str; + + while ((size_t)(iter - str) < len) { + if (*iter == '\\') + iter++; + + count++; + iter++; + } + + char *output = allocN(char, count + 1); + typed(size) offset = 0; + iter = str; + + while ((size_t)(iter - str) < len) { + if (*iter == '\\') { + iter++; + + switch (*iter) { + case 'b': + output[offset] = '\b'; + break; + case 'f': + output[offset] = '\f'; + break; + case 'n': + output[offset] = '\n'; + break; + case 'r': + output[offset] = '\r'; + break; + case 't': + output[offset] = '\t'; + break; + case '"': + output[offset] = '"'; + break; + case '\\': + output[offset] = '\\'; + break; + default: + return result_err(json_string)(JSON_ERROR_INVALID_VALUE); + } + } else { + output[offset] = *iter; + } + + offset++; + iter++; + } + + output[offset] = '\0'; + return result_ok(json_string)((typed(json_string))output); +} + +define_result_type(json_element_type) +define_result_type(json_element_value) +define_result_type(json_element) +define_result_type(json_entry) +define_result_type(json_string) +define_result_type(size) + diff --git a/fall-2025/sen-210/Executables/JobWizard/console_example/json.h b/fall-2025/sen-210/Executables/JobWizard/console_example/json.h new file mode 100644 index 0000000..8c2ff18 --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/console_example/json.h @@ -0,0 +1,163 @@ +#pragma once + +#include + +#ifndef __cplusplus +typedef unsigned int bool; +#define true (1) +#define false (0) +#endif + +#define typed(name) name##_t + +typedef const char *typed(json_string); +typedef bool typed(json_boolean); + +typedef union json_number_value_u typed(json_number_value); +typedef signed long typed(json_number_long); +typedef double typed(json_number_double); +typedef struct json_number_s typed(json_number); +typedef union json_element_value_u typed(json_element_value); +typedef struct json_element_s typed(json_element); +typedef struct json_entry_s typed(json_entry); +typedef struct json_object_s typed(json_object); +typedef struct json_array_s typed(json_array); + +#define result(name) name##_result_t +#define result_ok(name) name##_result_ok +#define result_err(name) name##_result_err +#define result_is_ok(name) name##_result_is_ok +#define result_is_err(name) name##_result_is_err +#define result_unwrap(name) name##_result_unwrap +#define result_unwrap_err(name) name##_result_unwrap_err +#define result_map_err(outer_name, inner_name, value) \ + result_err(outer_name)(result_unwrap_err(inner_name)(value)) +#define result_try(outer_name, inner_name, lvalue, rvalue) \ + result(inner_name) lvalue##_result = rvalue; \ + if (result_is_err(inner_name)(&lvalue##_result)) \ + return result_map_err(outer_name, inner_name, &lvalue##_result); \ + const typed(inner_name) lvalue = result_unwrap(inner_name)(&lvalue##_result); +#define declare_result_type(name) \ + typedef struct name##_result_s { \ + typed(json_boolean) is_ok; \ + union { \ + typed(name) value; \ + typed(json_error) err; \ + } inner; \ + } result(name); \ + result(name) result_ok(name)(typed(name)); \ + result(name) result_err(name)(typed(json_error)); \ + typed(json_boolean) result_is_ok(name)(result(name) *); \ + typed(json_boolean) result_is_err(name)(result(name) *); \ + typed(name) result_unwrap(name)(result(name) *); \ + typed(json_error) result_unwrap_err(name)(result(name) *); + +typedef enum json_element_type_e { + JSON_ELEMENT_TYPE_STRING = 0, + JSON_ELEMENT_TYPE_NUMBER, + JSON_ELEMENT_TYPE_OBJECT, + JSON_ELEMENT_TYPE_ARRAY, + JSON_ELEMENT_TYPE_BOOLEAN, + JSON_ELEMENT_TYPE_NULL +} typed(json_element_type); + +typedef enum json_number_type_e { + JSON_NUMBER_TYPE_LONG = 0, + JSON_NUMBER_TYPE_DOUBLE, +} typed(json_number_type); + +union json_number_value_u { + typed(json_number_long) as_long; + typed(json_number_double) as_double; +}; + +struct json_number_s { + typed(json_number_type) type; + typed(json_number_value) value; +}; + +union json_element_value_u { + typed(json_string) as_string; + typed(json_number) as_number; + typed(json_object) * as_object; + typed(json_array) * as_array; + typed(json_boolean) as_boolean; +}; + +struct json_element_s { + typed(json_element_type) type; + typed(json_element_value) value; +}; + +struct json_entry_s { + typed(json_string) key; + typed(json_element) element; +}; + +struct json_object_s { + typed(size) count; + typed(json_entry) * *entries; +}; + +struct json_array_s { + typed(size) count; + typed(json_element) * elements; +}; + +typedef enum json_error_e { + JSON_ERROR_EMPTY = 0, + JSON_ERROR_INVALID_TYPE, + JSON_ERROR_INVALID_KEY, + JSON_ERROR_INVALID_VALUE +} typed(json_error); + +declare_result_type(json_element_type) +declare_result_type(json_element_value) +declare_result_type(json_element) +declare_result_type(json_entry) +declare_result_type(json_string) +declare_result_type(size) + +/** + * @brief Parses a JSON string into a JSON element {json_element_t} + * with a fallible `result` type + * + * @param json_str The raw JSON string + * @return The parsed {json_element_t} wrapped in a `result` type + */ +result(json_element) json_parse(typed(json_string) json_str); + +/** + * @brief Tries to get the element by key. If not found, returns + * a {JSON_ERROR_INVALID_KEY} error + * + * @param object The object to find the key in + * @param key The key of the element to be found + * @return Either a {json_element_t} or {json_error_t} + */ +result(json_element) + json_object_find(typed(json_object) * object, typed(json_string) key); + +/** + * @brief Prints a JSON element {json_element_t} with proper + * indentation + * + * @param indent The number of spaces to indent each level by + */ +void json_print(typed(json_element) * element, int indent); + +/** + * @brief Frees a JSON element {json_element_t} from memory + * + * @param element The JSON element {json_element_t} to free + */ +void json_free(typed(json_element) * element); + +/** + * @brief Returns a string representation of JSON error {json_error_t} type + * + * @param error The JSON error enum {json_error_t} type + * @return The string representation + */ +typed(json_string) json_error_to_string(typed(json_error) error); + diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/.env_jobwizard b/fall-2025/sen-210/Executables/JobWizard/job_wizard/.env_jobwizard new file mode 100644 index 0000000..4a89bb4 --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/job_wizard/.env_jobwizard @@ -0,0 +1,2 @@ +JOBWIZARD_API_PORT=8889 +JOBWIZARD_DB_NAME=database/jobwizard_db diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/Notes_CSVImport.txt b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/Notes_CSVImport.txt new file mode 100644 index 0000000..c7d4021 --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/Notes_CSVImport.txt @@ -0,0 +1,12 @@ +To import CSV files into SQLITE: + +sqlite3 [dbname] +.mode csv +.import [file] [table] + +NOTE that even though the ID columns in the job_wizard DB are flagged as +"autoincrement", SQLITE does not really support this. So we have to provide ID +values explicitly in the CSV files. + +Above assumes no column headers in the file. I believe there is a way to tell +it to skip column headers. diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/jobs.csv b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/jobs.csv new file mode 100644 index 0000000..d473bcd --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/jobs.csv @@ -0,0 +1,12 @@ +1,sally@cmkl.ac.th,Front End Developer,Design and build attractive and highly usable UIs using React/JS,0,0,0,,1,2025-06-27 13:41 +700 +2,sally@cmkl.ac.th,Back End Developer,Microservices; REST APIs; Go language; Database design and implementation,2,2,40000,,1,2025-06-27 13:45 +700 +3,joe@cmkl.ac.th,HR Director,Manage onboarding - evaluation - staff retention - staff benefits for small university,3,5,95000,,1,2025-06-27 13:50 +700 +4,mark@cmkl.ac.th,Executive Secretary,Handle day to day management tasks for university president,1,3,35000,,1,2025-07-02 13:50 +700 +5,joe@cmkl.ac.th,Student Relations Officer,Assist students with planning study; gather feedback and complaints; interface with curriculum committee,2,3,42000,,1,2025-07-02 13:50 +700 +6,sally@cmkl.ac.th,Professor,Teaching and research to support the university ,4,5,95000,,1,2025-07-02 13:50 +700 +7,joe@cmkl.ac.th,Software Project Leader,Allocate tasks to software development team; monitor progress; train new developers; report to managment,3,4,50000,,1,2025-09-15 13:50 +700 +8,joe@cmkl.ac.th,Graphics Professional,"Create graphics content including imagery, videos, slide decks; acquire photos at university events",3,2,32600,,1,2025-09-15 13:50 +700 +9,joe@cmkl.ac.th,Janitor,Cleaning and maintenance,1,0,12500,lisa@outlook.com,0,2025-09-16 13:50 +700 +10,joe@cmkl.ac.th,Driver,Part time - Drive university van on schedule rounds; occasionally chauffer university president,1,3,18500,,1,2025-09-16 13:50 +700 +11,mark@cmkl.ac.th,CEO ,Top executive for promising tech start-up; compensation includes stock options,4,4,60000,,1,2025-09-17 13:50 +700 +12,sally@cmkl.ac.th,UX Designer,Design user interfaces for in-house software; guide developers in implementation; handle usability tests,3,3,38000,jim@gmail.com,0,2025-09-17 13:45 +700 diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/jobwizard_db b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/jobwizard_db new file mode 100644 index 0000000..5ee8241 Binary files /dev/null and b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/jobwizard_db differ diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/sampledata.xlsx b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/sampledata.xlsx new file mode 100644 index 0000000..6c8ce66 Binary files /dev/null and b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/sampledata.xlsx differ diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/users.csv b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/users.csv new file mode 100644 index 0000000..b31a64e --- /dev/null +++ b/fall-2025/sen-210/Executables/JobWizard/job_wizard/database/users.csv @@ -0,0 +1,5 @@ +1,sally@cmkl.ac.th,Sally,Goldin,“0879990088”,5,2025-06-05 +2,joe@cmkl.ac.th,Joe,Jenkins,“0329871233”,1,2025-06-22 +3,mark@cmkl.ac.th,Mark,Masters,“0770992324”,3,2025-06-26 +4,jim@gmail.com,James,Jamison,“0654329809”,2,2025-06-29 +5,lisa@outlook.com,Lisa,Roberts,“0567876666”,1,2025-09-17 diff --git a/fall-2025/sen-210/Executables/JobWizard/job_wizard/job_wizard b/fall-2025/sen-210/Executables/JobWizard/job_wizard/job_wizard new file mode 100755 index 0000000..f38b02b Binary files /dev/null and b/fall-2025/sen-210/Executables/JobWizard/job_wizard/job_wizard differ diff --git a/fall-2025/sen-210/Labs/Session1/README.md b/fall-2025/sen-210/Labs/Session1/README.md new file mode 100644 index 0000000..f3476d3 --- /dev/null +++ b/fall-2025/sen-210/Labs/Session1/README.md @@ -0,0 +1,13 @@ +# Lab 1: Introduction +Designing a user interface for the JobWizard CMD :D + +**Looking at...** +- What the user will do +- How will they do it? + +*Try to follow the Golden Rule of UIs* + +## FUCK JAVA AND PYTHON. DISGUSTING. + +## Reminder +Create account on Figma \ No newline at end of file diff --git a/fall-2025/sen-210/Labs/Session2/README.md b/fall-2025/sen-210/Labs/Session2/README.md new file mode 100644 index 0000000..e69de29 diff --git a/fall-2025/sen-210/README.md b/fall-2025/sen-210/README.md new file mode 100644 index 0000000..8a3198c --- /dev/null +++ b/fall-2025/sen-210/README.md @@ -0,0 +1 @@ +# SEN-210 - Designing User Interface \ No newline at end of file