Fri, 03 Jan 2025 19:18:00 +0100
implement index array to preserve order of json object members
relates to #526 and resolves #462
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 Fri Jan 03 17:16:49 2025 +0100 +++ b/src/cx/json.h Fri Jan 03 19:18:00 2025 +0100 @@ -231,6 +231,10 @@ * The key/value entries. */ CX_ARRAY_DECLARE(CxJsonObjValue, values); + /** + * The original indices to reconstruct the order in which the members were added. + */ + size_t *indices; }; /**
--- a/src/json.c Fri Jan 03 17:16:49 2025 +0100 +++ b/src/json.c Fri Jan 03 19:18:00 2025 +0100 @@ -67,13 +67,66 @@ } } -static int json_add_objvalue(CxJsonValue *obj, CxJsonObjValue member) { +static int json_add_objvalue(CxJsonValue *objv, CxJsonObjValue member) { assert(obj->type == CX_JSON_OBJECT); - CxArrayReallocator value_realloc = cx_array_reallocator(obj->allocator, NULL); - return cx_array_simple_add_sorted_a( - &value_realloc, obj->value.object.values, - member, json_cmp_objvalue + const CxAllocator * const al = objv->allocator; + CxJsonObject *obj = &(objv->value.object); + + // determine the index where we need to insert the new member + size_t index = cx_array_binary_search_sup( + obj->values, + obj->values_size, + sizeof(CxJsonObjValue), + &member, json_cmp_objvalue ); + + // is the name already present? + if (index < obj->values_size && 0 == json_cmp_objvalue(&member, &obj->values[index])) { + // free the original value + cx_strfree_a(al, &obj->values[index].name); + cxJsonValueFree(obj->values[index].value); + // replace the item + obj->values[index] = member; + + // nothing more to do + return 0; + } + + // determine the old capacity and reserve for one more element + CxArrayReallocator arealloc = cx_array_reallocator(al, NULL); + size_t oldcap = obj->values_capacity; + if (cx_array_simple_reserve_a(&arealloc, obj->values, 1)) return 1; + + // check the new capacity, if we need to realloc the index array + size_t newcap = obj->values_capacity; + if (newcap > oldcap) { + if (cxReallocateArray(al, &obj->indices, newcap, sizeof(size_t))) { + return 1; + } + } + + // check if append or insert + if (index < obj->values_size) { + // move the other elements + memmove( + &obj->values[index+1], + &obj->values[index], + (obj->values_size - index) * sizeof(CxJsonObjValue) + ); + // increase indices for the moved elements + for (size_t i = 0; i < obj->values_size ; i++) { + if (obj->indices[i] >= index) { + obj->indices[i]++; + } + } + } + + // insert the element and set the index + obj->values[index] = member; + obj->indices[obj->values_size] = index; + obj->values_size++; + + return 0; } static void token_destroy(CxJsonToken *token) { @@ -323,21 +376,22 @@ } static CxJsonValue* create_json_value(CxJson *json, CxJsonValueType type) { - CxJsonValue *v = cxMalloc(json->allocator, sizeof(CxJsonValue)); + CxJsonValue *v = cxCalloc(json->allocator, 1, sizeof(CxJsonValue)); if (v == NULL) return NULL; // LCOV_EXCL_LINE // initialize the value + v->type = type; + v->allocator = json->allocator; if (type == CX_JSON_ARRAY) { cx_array_initialize_a(json->allocator, v->value.array.array, 16); if (v->value.array.array == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE } else if (type == CX_JSON_OBJECT) { cx_array_initialize_a(json->allocator, v->value.object.values, 16); - if (v->value.object.values == NULL) goto create_json_value_exit_error; // LCOV_EXCL_LINE - } else { - memset(v, 0, sizeof(CxJsonValue)); + v->value.object.indices = cxCalloc(json->allocator, 16, sizeof(size_t)); + if (v->value.object.values == NULL || + v->value.object.indices == NULL) + goto create_json_value_exit_error; // LCOV_EXCL_LINE } - v->type = type; - v->allocator = json->allocator; // add the new value to a possible parent if (json->vbuf_size > 0) { @@ -377,7 +431,7 @@ return v; // LCOV_EXCL_START create_json_value_exit_error: - cxFree(json->allocator, v); + cxJsonValueFree(v); return NULL; // LCOV_EXCL_STOP } @@ -657,6 +711,7 @@ cx_strfree_a(value->allocator, &obj.values[i].name); } cxFree(value->allocator, obj.values); + cxFree(value->allocator, obj.indices); break; } case CX_JSON_ARRAY: { @@ -684,7 +739,18 @@ v->allocator = allocator; v->type = CX_JSON_OBJECT; cx_array_initialize_a(allocator, v->value.object.values, 16); - if (v->value.object.values == NULL) { cxFree(allocator, v); return NULL; } + if (v->value.object.values == NULL) { // LCOV_EXCL_START + cxFree(allocator, v); + return NULL; + // LCOV_EXCL_STOP + } + v->value.object.indices = cxCalloc(allocator, 16, sizeof(size_t)); + if (v->value.object.indices == NULL) { // LCOV_EXCL_START + cxFree(allocator, v->value.object.values); + cxFree(allocator, v); + return NULL; + // LCOV_EXCL_STOP + } return v; } @@ -822,20 +888,15 @@ } int cxJsonObjPut(CxJsonValue* obj, cxstring name, CxJsonValue* child) { - // TODO: optimize - issue #462 - for (size_t i = 0; i < obj->value.object.values_size; i++) { - if (0 == cx_strcmp(name, cx_strcast(obj->value.object.values[i].name))) { - // free the original value - cxJsonValueFree(obj->value.object.values[i].value); - obj->value.object.values[i].value = child; - return 0; - } - } - cxmutstr k = cx_strdup_a(obj->allocator, name); if (k.ptr == NULL) return -1; CxJsonObjValue kv = {k, child}; - return json_add_objvalue(obj, kv); + if (json_add_objvalue(obj, kv)) { + cx_strfree_a(obj->allocator, &k); + return 1; + } else { + return 0; + } } CxJsonValue* cxJsonObjPutObj(CxJsonValue* obj, cxstring name) { @@ -997,12 +1058,22 @@ expected++; } depth++; - CxIterator iter = cxJsonObjIter(value); - // TODO: unsorted output - realize after implementing index array - cx_foreach(CxJsonObjValue*, member, iter) { + size_t elem_count = value->value.object.values_size; + for (size_t look_idx = 0; look_idx < elem_count; look_idx++) { + // get the member either via index array or directly + size_t elem_idx = settings->sort_members + ? look_idx + : value->value.object.indices[look_idx]; + CxJsonObjValue *member = &value->value.object.values[elem_idx]; + if (settings->sort_members) { + depth++;depth--; + } + // possible indentation if (settings->pretty) { - if (cx_json_writer_indent(target, wfunc, settings, depth)) return 1; + if (cx_json_writer_indent(target, wfunc, settings, depth)) { + return 1; // LCOV_EXCL_LINE + } } // the name @@ -1024,7 +1095,7 @@ if (cx_json_write_rec(target, member->value, wfunc, settings, depth)) return 1; // end of object-value - if (iter.index < iter.elem_count - 1) { + if (look_idx < elem_count - 1) { const char *obj_value_sep = ",\n"; if (settings->pretty) { actual += wfunc(obj_value_sep, 1, 2, target); @@ -1049,7 +1120,6 @@ break; } case CX_JSON_ARRAY: { - // TODO: implement array wrapping actual += wfunc("[", 1, 1, target); expected++; CxIterator iter = cxJsonArrIter(value);
--- a/tests/test_json.c Fri Jan 03 17:16:49 2025 +0100 +++ b/tests/test_json.c Fri Jan 03 19:18:00 2025 +0100 @@ -730,8 +730,8 @@ 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("name2"), 7); 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); @@ -787,7 +787,6 @@ CxJsonWriter writer = cxJsonWriterCompact(); CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); - CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } @@ -820,7 +819,6 @@ CxJsonWriter writer = cxJsonWriterPretty(true); CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); - CX_TEST_ASSERT(cx_testing_allocator_verify(&talloc)); } cx_testing_allocator_destroy(&talloc); } @@ -856,6 +854,39 @@ cx_testing_allocator_destroy(&talloc); } +CX_TEST(test_json_write_pretty_preserve_order) { + CxTestingAllocator talloc; + cx_testing_allocator_init(&talloc); + CxAllocator *allocator = &talloc.base; + CX_TEST_DO { + cxstring expected = CX_STR( +"{\n" +" \"bool\": false,\n" +" \"int\": 47,\n" +" \"strings\": [\"hello\", \"world\"],\n" +" \"nested\": {\n" +" \"objects\": [{\n" +" \"name1\": 1,\n" +" \"name2\": 3\n" +" }, {\n" +" \"name2\": 7,\n" +" \"name1\": 3\n" +" }],\n" +" \"floats\": [3.1415, 47.11, 8.15],\n" +" \"literals\": [true, null, false],\n" +" \"ints\": [4, 8, 15, [16, 23], 42]\n" +" }\n" +"}" + ); + + CxJsonWriter writer = cxJsonWriterPretty(true); + writer.sort_members = false; + CX_TEST_CALL_SUBROUTINE(test_json_write_sub, allocator, expected, &writer); + 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"); @@ -877,6 +908,7 @@ cx_test_register(suite, test_json_write_default_format); cx_test_register(suite, test_json_write_pretty_default_spaces); cx_test_register(suite, test_json_write_pretty_default_tabs); + cx_test_register(suite, test_json_write_pretty_preserve_order); return suite; }