Thu, 02 Jan 2025 20:58:32 +0100
implement JSON pretty printing - relates to #526
src/json.c | file | annotate | diff | comparison | revisions | |
tests/test_json.c | file | annotate | diff | comparison | revisions |
--- a/src/json.c Thu Jan 02 19:07:56 2025 +0100 +++ b/src/json.c Thu Jan 02 20:58:32 2025 +0100 @@ -944,6 +944,36 @@ }; } +static int cx_json_writer_indent( + void *target, + cx_write_func wfunc, + const CxJsonWriter *settings, + unsigned int depth +) { + if (depth == 0) return 0; + + // determine the width and characters to use + const char* indent; // for 32 prepared chars + size_t width = depth; + if (settings->indent_space) { + if (settings->indent == 0) return 0; + width *= settings->indent; + indent = " "; + } else { + indent = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; + } + + // calculate the number of write calls and write + size_t full = width / 32; + size_t remaining = width % 32; + for (size_t i = 0; i < full; i++) { + if (32 != wfunc(indent, 1, 32, target)) return 1; + } + if (remaining != wfunc(indent, 1, remaining, target)) return 1; + + return 0; +} + int cx_json_write_rec( void *target, @@ -952,9 +982,8 @@ const CxJsonWriter *settings, unsigned int depth ) { - // TODO: implement indentation - // keep track of written items + // the idea is to reduce the number of jumps for error checking size_t actual = 0, expected = 0; // small buffer for number to string conversions @@ -971,8 +1000,14 @@ actual += wfunc(begin_obj, 1, 1, target); expected++; } + depth++; CxIterator iter = cxJsonObjIter(value); cx_foreach(CxJsonObjValue*, member, iter) { + // possible indentation + if (settings->pretty) { + if (cx_json_writer_indent(target, wfunc, settings, depth)) return 1; + } + // the name actual += wfunc("\"", 1, 1, target); // TODO: escape the string @@ -989,13 +1024,7 @@ } // the value - if (0 == cx_json_write_rec( - target, member->value, - wfunc, settings, depth + 1) - ) { - actual++; // count the nested values as one item - } - expected++; + if (cx_json_write_rec(target, member->value, wfunc, settings, depth)) return 1; // end of object-value if (iter.index < iter.elem_count - 1) { @@ -1014,6 +1043,10 @@ } } } + depth--; + if (settings->pretty) { + if (cx_json_writer_indent(target, wfunc, settings, depth)) return 1; + } actual += wfunc("}", 1, 1, target); expected++; break; @@ -1024,14 +1057,10 @@ expected++; CxIterator iter = cxJsonArrIter(value); cx_foreach(CxJsonValue*, element, iter) { - // TODO: pretty printing obj elements vs. primitives - if (0 == cx_json_write_rec( + if (cx_json_write_rec( target, element, - wfunc, settings, depth + 1) - ) { - actual++; // count the nested values as one item - } - expected++; + wfunc, settings, depth) + ) return 1; if (iter.index < iter.elem_count - 1) { const char *arr_value_sep = ", ";
--- a/tests/test_json.c Thu Jan 02 19:07:56 2025 +0100 +++ b/tests/test_json.c Thu Jan 02 20:58:32 2025 +0100 @@ -740,6 +740,77 @@ CxBuffer buf; cxBufferInit(&buf, NULL, 256, NULL, CX_BUFFER_DEFAULT); int result = cxJsonWrite(&buf, obj, (cx_write_func) cxBufferWrite, NULL); + cxBufferTerminate(&buf); // makes debugging easier + 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); +} + +CX_TEST(test_json_write_pretty_default_spaces) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *allocator = &talloc.base; + CX_TEST_DO { + // expected value + cxstring expected = CX_STR( +"{\n" +" \"bool\": false,\n" +" \"int\": 47,\n" +" \"nested\": {\n" +" \"floats\": [3.1415, 47.11, 8.15],\n" +" \"ints\": [4, 8, 15, [16, 23], 42],\n" +" \"literals\": [true, null, false],\n" +" \"objects\": [{\n" +" \"name1\": 1,\n" +" \"name2\": 3\n" +" }, {\n" +" \"name1\": 3,\n" +" \"name2\": 7\n" +" }]\n" +" },\n" +" \"strings\": [\"hello\", \"world\"]\n" +"}" + ); + + // create the value + CxJsonValue *obj = cxJsonCreateObj(allocator); + cxJsonObjPutLiteral(obj, CX_STR("bool"), CX_JSON_FALSE); + cxJsonObjPutNumber(obj, CX_STR("int"), 47); // purposely use PutNumber to put an int + CxJsonValue *strings = cxJsonObjPutArr(obj, CX_STR("strings")); + cxJsonArrAddCxStrings(strings, (cxstring[]) {CX_STR("hello"), CX_STR("world")}, 2); + CxJsonValue *nested = cxJsonObjPutObj(obj, CX_STR("nested")); + CxJsonValue *objects = cxJsonObjPutArr(nested, CX_STR("objects")); + CxJsonValue *obj_in_arr[2] = {cxJsonCreateObj(allocator), cxJsonCreateObj(allocator)}; + cxJsonObjPutInteger(obj_in_arr[0], CX_STR("name1"), 1); + cxJsonObjPutInteger(obj_in_arr[0], CX_STR("name2"), 3); + cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name1"), 3); + cxJsonObjPutInteger(obj_in_arr[1], CX_STR("name2"), 7); + cxJsonArrAddValues(objects, obj_in_arr, 2); + 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); + CxJsonValue *ints = cxJsonObjPutArr(nested, CX_STR("ints")); + cxJsonArrAddIntegers(ints, (int64_t[]){4, 8, 15}, 3); + CxJsonValue *nested_array = cxJsonCreateArr(allocator); + cxJsonArrAddValues(ints, &nested_array, 1); + cxJsonArrAddIntegers(nested_array, (int64_t[]){16, 23}, 2); + cxJsonArrAddIntegers(ints, (int64_t[]){42}, 1); + + // write it to a buffer + CxBuffer buf; + cxBufferInit(&buf, NULL, 512, NULL, CX_BUFFER_DEFAULT); + CxJsonWriter writer = cxJsonWriterPretty(true); + int result = cxJsonWrite(&buf, obj, (cx_write_func) cxBufferWrite, &writer); + cxBufferTerminate(&buf); // makes debugging easier CX_TEST_ASSERT(result == 0); // compare the string @@ -772,6 +843,7 @@ 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); + cx_test_register(suite, test_json_write_pretty_default_spaces); return suite; }