first mvp for the json writer - relates to #526

Wed, 01 Jan 2025 15:33:41 +0100

author
Mike Becker <universe@uap-core.de>
date
Wed, 01 Jan 2025 15:33:41 +0100
changeset 1072
c89283cd559b
parent 1071
028cb6d22197
child 1073
13c8a92625d4

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

mercurial