implement JSON pretty printing - relates to #526

Thu, 02 Jan 2025 20:58:32 +0100

author
Mike Becker <universe@uap-core.de>
date
Thu, 02 Jan 2025 20:58:32 +0100
changeset 1078
ffa8bb4e9288
parent 1077
911a154dd469
child 1079
4e1872151fb6

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;
 }

mercurial