Wed, 01 Jan 2025 15:33:41 +0100
first mvp for the json writer - relates to #526
src/cx/json.h | file | annotate | diff | comparison | revisions | |
src/json.c | file | annotate | diff | comparison | revisions | |
tests/test_json.c | file | annotate | diff | comparison | revisions |
--- a/src/cx/json.h Wed Jan 01 15:26:50 2025 +0100 +++ b/src/cx/json.h Wed Jan 01 15:33:41 2025 +0100 @@ -432,6 +432,52 @@ typedef enum cx_json_status CxJsonStatus; /** + * The JSON writer settings. + */ +struct cx_json_writer_s { + bool pretty; + bool sort_members; + uint8_t frac_max_digits; + bool indent_space; + uint8_t indent; + bool wrap_array; + uint16_t wrap_threshold; +}; + +/** + * Typedef for the json writer. + */ +typedef struct cx_json_writer_s CxJsonWriter; + + +/** + * Writes a JSON value to a buffer or stream. + * + * This function blocks until all data is written or an error when trying + * to write data occurs. + * The write operation is not atomic in the sense that it might happen + * that the data is only partially written when an error occurs with no + * way to indicate how much data was written. + * To avoid this problem, you can use a CxBuffer as \p target which is + * unlikely to fail a write operation and either use the buffer's flush + * feature to relay the data or use the data in the buffer manually to + * write it to the actual target. + * + * @param target the buffer or stream where to write to + * @param value the value that shall be written + * @param wfunc the write function to use + * @param settings formatting settings (or \c NULL to use a compact default) + * @return zero on success, non-zero when no or not all data could be written + */ +cx_attr_nonnull_arg(1, 2, 3) +int cxJsonWrite( + void* target, + const CxJsonValue* value, + cx_write_func wfunc, + const CxJsonWriter* settings +); + +/** * Initializes the json interface. * * @param json the json interface @@ -1150,6 +1196,22 @@ CxIterator cxJsonArrIter(const CxJsonValue *value); /** + * Returns an iterator over the JSON object members. + * + * The iterator yields values of type \c CxJsonObjValue* which + * contain the name and value of the member. + * + * If the \p value is not a JSON object, the behavior is undefined. + * + * @param value the JSON value + * @return an iterator over the object members + * @see cxJsonIsObject() + */ +cx_attr_nonnull +cx_attr_nodiscard +CxIterator cxJsonObjIter(const CxJsonValue *value); + +/** * @copydoc cxJsonObjGet() */ cx_attr_nonnull
--- a/src/json.c Wed Jan 01 15:26:50 2025 +0100 +++ b/src/json.c Wed Jan 01 15:33:41 2025 +0100 @@ -34,6 +34,7 @@ #include <assert.h> #include <stdio.h> #include <errno.h> +#include <inttypes.h> /* * RFC 8259 @@ -889,37 +890,19 @@ return value->value.array.array[index]; } -static void *cx_json_iter_current(const void *it) { - const CxIterator *iter = it; - return *(CxJsonValue**)iter->elem_handle; -} - -static bool cx_json_iter_valid(const void *it) { - const CxIterator *iter = it; - return iter->index < iter->elem_count; -} - -static void cx_json_iter_next(void *it) { - CxIterator *iter = it; - iter->index++; - iter->elem_handle = (char *) iter->elem_handle + sizeof(void *); +CxIterator cxJsonArrIter(const CxJsonValue *value) { + return cxIteratorPtr( + value->value.array.array, + value->value.array.array_size + ); } -CxIterator cxJsonArrIter(const CxJsonValue *value) { - CxIterator iter; - - iter.index = 0; - iter.elem_count = value->value.array.array_size; - iter.src_handle.m = value->value.array.array; - iter.elem_handle = iter.src_handle.m; - iter.elem_size = sizeof(CxJsonValue*); - iter.base.valid = cx_json_iter_valid; - iter.base.current = cx_json_iter_current; - iter.base.next = cx_json_iter_next; - iter.base.remove = false; - iter.base.mutating = false; - - return iter; +CxIterator cxJsonObjIter(const CxJsonValue *value) { + return cxIterator( + value->value.object.values, + sizeof(CxJsonObjValue), + value->value.object.values_size + ); } CxJsonValue *cx_json_obj_get_cxstr(const CxJsonValue *value, cxstring name) { @@ -930,3 +913,186 @@ return member->value; } } + +static const CxJsonWriter cx_json_writer_default = { + false, + true, + 255, + false, + 0, + false, + 0 +}; + +// TODO: add default for pretty printing and add functions to create default structs + + +int cx_json_write_rec( + void *target, + const CxJsonValue *value, + cx_write_func wfunc, + const CxJsonWriter *settings, + unsigned int depth +) { + // TODO: implement indentation + + // keep track of written items + size_t actual = 0, expected = 0; + + // small buffer for number to string conversions + char numbuf[32]; + + // recursively write the values + switch (value->type) { + case CX_JSON_OBJECT: { + const char *begin_obj = "{\n"; + if (settings->pretty) { + actual += wfunc(begin_obj, 1, 2, target); + expected += 2; + } else { + actual += wfunc(begin_obj, 1, 1, target); + expected++; + } + CxIterator iter = cxJsonObjIter(value); + cx_foreach(CxJsonObjValue*, member, iter) { + // the name + actual += wfunc("\"", 1, 1, target); + // TODO: escape the string + actual += wfunc(member->name.ptr, 1, + member->name.length, target); + actual += wfunc("\"", 1, 1, target); + const char *obj_name_sep = ": "; + if (settings->pretty) { + actual += wfunc(obj_name_sep, 1, 2, target); + expected += 4 + member->name.length; + } else { + actual += wfunc(obj_name_sep, 1, 1, target); + expected += 3 + member->name.length; + } + + // the value + if (0 == cx_json_write_rec( + target, member->value, + wfunc, settings, depth + 1) + ) { + actual++; // count the nested values as one item + } + expected++; + + // end of object-value + if (iter.index < iter.elem_count - 1) { + const char *obj_value_sep = ",\n"; + if (settings->pretty) { + actual += wfunc(obj_value_sep, 1, 2, target); + expected += 2; + } else { + actual += wfunc(obj_value_sep, 1, 1, target); + expected++; + } + } else { + if (settings->pretty) { + actual += wfunc("\n", 1, 1, target); + expected ++; + } + } + } + actual += wfunc("}", 1, 1, target); + expected++; + break; + } + case CX_JSON_ARRAY: { + // TODO: implement array wrapping + actual += wfunc("[", 1, 1, target); + expected++; + CxIterator iter = cxJsonArrIter(value); + cx_foreach(CxJsonValue*, element, iter) { + // TODO: pretty printing obj elements vs. primitives + if (0 == cx_json_write_rec( + target, element, + wfunc, settings, depth + 1) + ) { + actual++; // count the nested values as one item + } + expected++; + + if (iter.index < iter.elem_count - 1) { + const char *arr_value_sep = ", "; + if (settings->pretty) { + actual += wfunc(arr_value_sep, 1, 2, target); + expected += 2; + } else { + actual += wfunc(arr_value_sep, 1, 1, target); + expected++; + } + } + } + actual += wfunc("]", 1, 1, target); + expected++; + break; + } + case CX_JSON_STRING: { + actual += wfunc("\"", 1, 1, target); + // TODO: escape the string + actual += wfunc(value->value.string.ptr, 1, + value->value.string.length, target); + actual += wfunc("\"", 1, 1, target); + expected += 2 + value->value.string.length; + break; + } + case CX_JSON_NUMBER: { + // TODO: locale bullshit + // TODO: formatting settings + snprintf(numbuf, 32, "%g", value->value.number); + size_t len = strlen(numbuf); + actual += wfunc(numbuf, 1, len, target); + expected += len; + break; + } + case CX_JSON_INTEGER: { + snprintf(numbuf, 32, "%" PRIi64, value->value.integer); + size_t len = strlen(numbuf); + actual += wfunc(numbuf, 1, len, target); + expected += len; + break; + } + case CX_JSON_LITERAL: { + if (value->value.literal == CX_JSON_TRUE) { + actual += wfunc("true", 1, 4, target); + expected += 4; + } else if (value->value.literal == CX_JSON_FALSE) { + actual += wfunc("false", 1, 5, target); + expected += 5; + } else { + actual += wfunc("null", 1, 4, target); + expected += 4; + } + break; + } + case CX_JSON_NOTHING: { + // deliberately supported as an empty string! + // users might want to just write the result + // of a get operation without testing the value + // and therefore this should not blow up + break; + } + default: assert(false); // LCOV_EXCL_LINE + } + + return expected != actual; +} + +int cxJsonWrite( + void *target, + const CxJsonValue *value, + cx_write_func wfunc, + const CxJsonWriter *settings +) { + if (settings == NULL) { + settings = &cx_json_writer_default; + } + assert(target != NULL); + assert(value != NULL); + assert(wfunc != NULL); + + return cx_json_write_rec(target, value, wfunc, settings, 0); +}
--- a/tests/test_json.c Wed Jan 01 15:26:50 2025 +0100 +++ b/tests/test_json.c Wed Jan 01 15:33:41 2025 +0100 @@ -705,6 +705,46 @@ cx_testing_allocator_destroy(&talloc); } +CX_TEST(test_json_write_default_format) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *allocator = &talloc.base; + CX_TEST_DO { + // expected value + cxstring expected = CX_STR("{\"bool\":false,\"nested\":{\"floats\":[3.1415,47.11,8.15],\"ints\":[4,8,15,16,23,42],\"literals\":[true,null,false],\"string\":\"test\"},\"num\":47.11,\"strings\":[\"hello\",\"world\"]}"); + + // create the value + CxJsonValue *obj = cxJsonCreateObj(allocator); + cxJsonObjPutLiteral(obj, CX_STR("bool"), CX_JSON_FALSE); + cxJsonObjPutNumber(obj, CX_STR("num"), 47.11); + CxJsonValue *strings = cxJsonObjPutArr(obj, CX_STR("strings")); + cxJsonArrAddCxStrings(strings, (cxstring[]) {CX_STR("hello"), CX_STR("world")}, 2); + CxJsonValue *nested = cxJsonObjPutObj(obj, CX_STR("nested")); + cxJsonObjPutString(nested, CX_STR("string"), "test"); + cxJsonArrAddNumbers(cxJsonObjPutArr(nested, CX_STR("floats")), + (double[]){3.1415, 47.11, 8.15}, 3); + cxJsonArrAddLiterals(cxJsonObjPutArr(nested, CX_STR("literals")), + (CxJsonLiteral[]){CX_JSON_TRUE, CX_JSON_NULL, CX_JSON_FALSE}, 3); + cxJsonArrAddIntegers(cxJsonObjPutArr(nested, CX_STR("ints")), + (int64_t[]){4, 8, 15, 16, 23, 42}, 6); + + // write it to a buffer + CxBuffer buf; + cxBufferInit(&buf, NULL, 256, NULL, CX_BUFFER_DEFAULT); + int result = cxJsonWrite(&buf, obj, (cx_write_func) cxBufferWrite, NULL); + CX_TEST_ASSERT(result == 0); + + // compare the string + CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), expected)); + + // destroy everything + cxBufferDestroy(&buf); + cxJsonValueFree(obj); + CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); + } + cx_testing_allocator_destroy(&talloc); +} + CxTestSuite *cx_test_suite_json(void) { CxTestSuite *suite = cx_test_suite_new("json"); @@ -723,6 +763,7 @@ cx_test_register(suite, test_json_allocator); cx_test_register(suite, test_json_allocator_parse_error); cx_test_register(suite, test_json_create_value); + cx_test_register(suite, test_json_write_default_format); return suite; }