This is an automated email from the ASF dual-hosted git repository. pnoltes pushed a commit to branch feature/685-properties-json-serialization in repository https://gitbox.apache.org/repos/asf/celix.git
commit 8e03a828be1f707df23b3fe77b62d491a3adf527 Author: Pepijn Noltes <pnol...@apache.org> AuthorDate: Wed Apr 10 23:27:59 2024 +0200 gh-685: Add support for a flat and nested flag for prop encoding --- libs/utils/gtest/src/CelixUtilsTestSuite.cc | 32 ++ .../utils/gtest/src/PropertiesEncodingTestSuite.cc | 435 ++++++++++++++------- libs/utils/include/celix_properties.h | 81 ++-- libs/utils/include/celix_utils.h | 56 +++ libs/utils/src/properties_encoding.c | 208 +++++++--- 5 files changed, 588 insertions(+), 224 deletions(-) diff --git a/libs/utils/gtest/src/CelixUtilsTestSuite.cc b/libs/utils/gtest/src/CelixUtilsTestSuite.cc index 26bef64b..95e143d4 100644 --- a/libs/utils/gtest/src/CelixUtilsTestSuite.cc +++ b/libs/utils/gtest/src/CelixUtilsTestSuite.cc @@ -315,6 +315,38 @@ TEST_F(UtilsTestSuite, WriteOrCreateStringTest) { celix_utils_freeStringIfNotEqual(buffer2, out2); } +TEST_F(UtilsTestSuite, WriteOrCreateStringGuardTest) { + // Given a small buffer + char buffer[16]; + + { + // When writing a string that fits in the buffer + char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "abc"); + + // Then the str is equal to the buffer (in this case no malloc was needed) + EXPECT_EQ(buffer, str); + + // And using celix_auto with a string guard + celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buffer, str); + + // Then the guard will not free the string when going out of scope + } + + { + // When writing a string that does not fit in the buffer + char* str = celix_utils_writeOrCreateString( + buffer, sizeof(buffer), "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"); + + // Then the str is not equal to the buffer (in this case a malloc was needed) + EXPECT_NE(buffer, str); + + // And using celix_auto with a string guard + celix_auto(celix_utils_string_guard_t) guard = celix_utils_stringGuard_init(buffer, str); + + // Then the guard will free the string when going out of scope + } +} + TEST_F(UtilsTestSuite, StrDupAndStrLenTest) { celix_autofree char* str = celix_utils_strdup("abc"); ASSERT_NE(nullptr, str); diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc index d346f5f6..84ed5d0a 100644 --- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc @@ -32,7 +32,7 @@ class PropertiesSerializationTestSuite : public ::testing::Test { PropertiesSerializationTestSuite() { celix_err_resetErrors(); } }; -TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { +TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { //Given an empty properties object celix_autoptr(celix_properties_t) props = celix_properties_create(); @@ -41,8 +41,8 @@ TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream, 0); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains an empty JSON object @@ -50,7 +50,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) { EXPECT_STREQ("{}", buf); } -TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) { //Given a properties object with single values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -65,8 +65,8 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream, 0); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties @@ -85,7 +85,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) { //Given a NAN, INF and -INF value auto keys = {"NAN", "INF", "-INF"}; for (const auto& key : keys) { @@ -102,7 +102,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest //Then saving the properties to the stream fails, because JSON does not support NAN, INF and -INF celix_err_resetErrors(); - auto status = celix_properties_encodeToStream(props, stream, 0); + auto status = celix_properties_saveToStream(props, stream, 0); EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); //And an error msg is added to celix_err @@ -111,7 +111,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithNaNAndInfValuesTest } -TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { // Given a properties object with array list values celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_array_list_t* list1 = celix_arrayList_createStringArray(); @@ -141,7 +141,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { FILE* stream = open_memstream(&buf, &bufLen); // When saving the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + auto status = celix_properties_saveToStream(props, stream, 0); ASSERT_EQ(CELIX_SUCCESS, status); // Then the stream contains the JSON representation snippets of the properties @@ -161,7 +161,7 @@ TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) { } -TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) { +TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) { //Given a properties object with an empty array list of with el types string, long, double, bool, version celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_assignArrayList(props, "key1", celix_arrayList_createStringArray()); @@ -171,21 +171,28 @@ TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) { celix_properties_assignArrayList(props, "key5", celix_arrayList_createVersionArray()); EXPECT_EQ(5, celix_properties_size(props)); - //And an in-memory stream - celix_autofree char* buf = nullptr; - size_t bufLen = 0; - FILE* stream = open_memstream(&buf, &bufLen); + //When saving the properties to a string + char* output = nullptr; + auto status = celix_properties_saveToString(props, 0, &output); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //Then the save went ok ASSERT_EQ(CELIX_SUCCESS, status); - //Then the stream contains an empty JSON object, because empty arrays are treated as unset - fclose(stream); - EXPECT_STREQ("{}", buf); + //And the output contains an empty JSON object, because empty arrays are treated as unset + EXPECT_STREQ("{}", output); + + //When saving the properties to a string with an error on empty array flag + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS, &output); + + //Then the save fails, because the empty array generates an error + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); } -TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { +TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) { //Given a properties object with jpath keys celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1", "value1"); @@ -200,8 +207,8 @@ TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { size_t bufLen = 0; FILE* stream = open_memstream(&buf, &bufLen); - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to the stream + auto status = celix_properties_saveToStream(props, stream, CELIX_PROPERTIES_ENCODE_NESTED); ASSERT_EQ(CELIX_SUCCESS, status); //Then the stream contains the JSON representation snippets of the properties @@ -219,90 +226,185 @@ TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) { json_decref(root); } -TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) { +TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) { + // note this tests depends on the key iteration order for properties and + // properties key order is based on hash order of the keys, so this test can change if the string hash map + // implementation changes. + //Given a properties object with jpath keys that collide celix_autoptr(celix_properties_t) props = celix_properties_create(); celix_properties_set(props, "key1/key2/key3", "value1"); - celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" - celix_properties_set(props, "key4/key5/key6", "value3"); - celix_properties_set(props, "key4/key5/key6/key7", "value4"); //collision with field "key3/key4/key5" + celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2/key3" -> overwrite + celix_properties_set(props, "key4/key5/key6/key7", "value4"); + celix_properties_set(props, "key4/key5/key6", "value3"); //collision with field "key4/key5/key6/key7" -> overwrite - //And an in-memory stream - celix_autofree char* buf = nullptr; - size_t bufLen = 0; - FILE* stream = open_memstream(&buf, &bufLen); - - //When encoding the properties to the stream - auto status = celix_properties_encodeToStream(props, stream, 0); + //When saving the properties to a string + celix_autofree char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); ASSERT_EQ(CELIX_SUCCESS, status); - //Then the stream contains the JSON representation snippets of the properties - fclose(stream); - EXPECT_NE(nullptr, strstr(buf, R"("key1":{"key2":{"key3":"value1"}})")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key1/key2":"value2")")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key4/key5/key6":"value3")")) << "JSON: " << buf; - EXPECT_NE(nullptr, strstr(buf, R"("key4":{"key5":{"key6":{"key7":"value4"}}})")) << "JSON: " << buf; - //Note whether "key1/key2/key3" or "key1/key2" is serializer first depends on the hash order of the keys, - //so this test can change if the string hash map implementation changes. + //Then the stream contains the JSON representation of the properties with the collisions resolved + EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"},"key4":{"key5":{"key6":"value3"}}})")) + << "JSON: " << output; //And the buf is a valid JSON object json_error_t error; - json_t* root = json_loads(buf, 0, &error); + json_t* root = json_loads(output, 0, &error); EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; json_decref(root); } -//TODO check desired behaviour, currently every "/" leads to a new object (except if an collision occurs) -//TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSpecialKeyNamesTest) { -// //Given a properties set with special key names (slashes) -// celix_autoptr(celix_properties_t) props = celix_properties_create(); -// celix_properties_set(props, "/", "value1"); -// celix_properties_set(props, "keyThatEndsWithSlash/", "value2"); -// celix_properties_set(props, "key//With//Double//Slash", "value3"); -// celix_properties_set(props, "object/", "value5"); -// celix_properties_set(props, "object//", "value4"); -// celix_properties_set(props, "object/keyThatEndsWithSlash/", "value6"); -// celix_properties_set(props, "object/key//With//Double//Slash", "value7"); -// -// //And an in-memory stream -// celix_autofree char* buf = nullptr; -// size_t bufLen = 0; -// FILE* stream = open_memstream(&buf, &bufLen); -// -// //When encoding the properties to the stream -// auto status = celix_properties_encodeToStream(props, stream, 0); -// ASSERT_EQ(CELIX_SUCCESS, status); -// -// std::cout << buf << std::endl; -// -// //Then the stream contains the JSON representation snippets of the properties -// fclose(stream); -// EXPECT_NE(nullptr, strstr(buf, R"("/":"value1")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value2")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value3")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("object/":"value5")")) << "JSON: " << buf; -// EXPECT_NE(nullptr, strstr(buf, R"("/":"value5")")) << "JSON: " << buf; //child of object -// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value6")")) << "JSON: " << buf; //child of object -// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value7")")) << "JSON: " << buf; //child of object -// -// -// //And the buf is a valid JSON object -// json_error_t error; -// json_t* root = json_loads(buf, 0, &error); -// EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; -// json_decref(root); -//} - - -TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) { + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyNamesWithSlashesTest) { + //Given a properties set with key names with slashes + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "a/key/name/with/slashes", "value1"); + //TODO test separately celix_properties_set(props, "/", "value2"); + celix_properties_set(props, "/keyThatStartsWithSlash", "value3"); + //TODO test separately celix_properties_set(props, "//keyThatStartsWithDoubleSlashes", "value4"); + celix_properties_set(props, "keyThatEndsWithSlash/", "value5"); + celix_properties_set(props, "keyThatEndsWithDoubleSlashes//", "value6"); + celix_properties_set(props, "key//With//Double//Slashes", "value7"); + celix_properties_set(props, "object/keyThatEndsWithSlash/", "value8"); + celix_properties_set(props, "object/keyThatEndsWithDoubleSlashes//", "value9"); + celix_properties_set(props, "object/key//With//Double//Slashes", "value10"); + + + //When saving the properties to a string + char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + ASSERT_EQ(CELIX_SUCCESS, status); + + //Then the out contains the JSON representation snippets of the properties + EXPECT_NE(nullptr, strstr(output, R"("a":{"key":{"name":{"with":{"slashes":"value1"}}}})")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("keyThatStartsWithSlash":"value3")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value5")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value6")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value7")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value8")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("":"value9")")) << "JSON: " << output; + EXPECT_NE(nullptr, strstr(output, R"("Slashes":"value10")")) << "JSON: " << output; + + //And the output is a valid JSON object + json_error_t error; + json_t* root = json_loads(output, 0, &error); + EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; + + + //And the structure for (e.g.) value10 is correct + json_t* node = json_object_get(root, "object"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "key"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, ""); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "With"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, ""); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "Double"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, ""); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_object(node)); + node = json_object_get(node, "Slashes"); + ASSERT_NE(nullptr, node); + ASSERT_TRUE(json_is_string(node)); + EXPECT_STREQ("value10", json_string_value(node)); + + json_decref(root); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithKeyCollision) { + // note this tests depends on the key iteration order for properties and + // properties key order is based on hash order of the keys, so this test can change if the string hash map + // implementation changes. + + //Given a properties that contains keys that will collide with an existing JSON object + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1/key2/key3", "value1"); + celix_properties_set(props, "key1/key2", "value2"); //collision with object "key1/key2" -> overwrite + + //When saving the properties to a string + char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_NESTED, &output); + + //Then the save succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And both keys are serialized (one as a flat key) (flat key name is whitebox knowledge) + EXPECT_NE(nullptr, strstr(output, R"({"key1":{"key2":"value2"}})")) << "JSON: " << output; + + //When saving the properties to a string with the error on key collision flag + status = celix_properties_saveToString( + props, CELIX_PROPERTIES_ENCODE_NESTED | CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS, &output); + + //Then the save fails, because the keys collide + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithAndWithoutStrictFlagTest) { + //Given a properties set with an empty array list + celix_autoptr(celix_properties_t) props = celix_properties_create(); + auto* list = celix_arrayList_createStringArray(); + celix_properties_assignArrayList(props, "key1", list); + + //When saving the properties to a string without the strict flag + char* output = nullptr; + auto status = celix_properties_saveToString(props, 0, &output); + + //Then the save succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + //When saving the properties to a string with the strict flag + status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_STRICT, &output); + + //Then the save fails, because the empty array generates an error + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithPrettyPrintTest) { + //Given a properties set with 2 keys + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_set(props, "key1", "value1"); + celix_properties_set(props, "key2", "value2"); + + //When saving the properties to a string with pretty print + char* output = nullptr; + auto status = celix_properties_saveToString(props, CELIX_PROPERTIES_ENCODE_PRETTY, &output); + + //Then the save succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the output contains the JSON representation snippets of the properties with pretty print (2 indent spaces and + // newlines) + auto* expected = "{\n \"key2\": \"value2\",\n \"key1\": \"value1\"\n}"; + EXPECT_STREQ(expected, output); +} + +TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { //Given an empty JSON object const char* json = "{}"; FILE* stream = fmemopen((void*)json, strlen(json), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object is empty @@ -311,7 +413,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) { fclose(stream); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) { //Given a JSON object with single values for types string, long, double, bool and version const char* jsonInput = R"({ "strKey":"strValue", @@ -324,9 +426,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { //And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object contains the single values @@ -341,7 +443,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) { EXPECT_STREQ("1.2.3.qualifier", vStr); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) { //Given a JSON object with array values for types string, long, double, bool and version const char* jsonInput = R"({ "strArr":["value1","value2"], @@ -356,9 +458,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { //And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); //Then the properties object contains the array values @@ -430,7 +532,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) { EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3)); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { auto invalidInputs = { R"({)", // invalid JSON (caught by jansson) R"([])", // unsupported JSON (top level array not supported) @@ -440,9 +542,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When decoding the properties from the stream + //When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); //Then loading fails EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -455,25 +557,31 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) { } } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithEmptyArrayTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithEmptyArrayTest) { //Given a JSON object with an empty array - auto* emptyArrays = R"({"key1":[]})"; + auto* inputJSON = R"({"key1":[]})"; - //And a stream with the JSON object - FILE* stream = fmemopen((void*)emptyArrays, strlen(emptyArrays), "r"); - - //When decoding the properties from the stream + //When loading the properties from string celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromString2(inputJSON, 0, &props); //Then loading succeeds ASSERT_EQ(CELIX_SUCCESS, status); //And the properties object is empty, because empty arrays are treated as unset EXPECT_EQ(0, celix_properties_size(props)); + + //When loading the properties from string with a strict flag + status = celix_properties_loadFromString2(inputJSON, CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS, &props); + + //Then loading fails, because the empty array generates an error + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And at least one error message is added to celix_err + ASSERT_GE(celix_err_getErrorCount(), 1); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithNestedObjectsTest) { // Given a complex JSON object const char* jsonInput = R"({ "key1":"value1", @@ -495,9 +603,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) // And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - // When decoding the properties from the stream + // When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); ASSERT_EQ(CELIX_SUCCESS, status); // Then the properties object contains the nested objects @@ -510,40 +618,79 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsTest) EXPECT_EQ(6, celix_properties_getLong(props, "object3/object4/key6", 0)); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPathCollisionTest) { - // Given a complex JSON object with jpath keys that collide +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithDuplicatesTest) { + // Given a complex JSON object with duplicate keys + const char* jsonInput = R"({ + "key":2, + "key":3 + })"; + + // When loading the properties from a string. + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromString2(jsonInput, 0, &props); + + // Then loading succeeds + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the properties object contains the last values of the jpath keys + EXPECT_EQ(1, celix_properties_size(props)); + EXPECT_EQ(3, celix_properties_getLong(props, "key", 0)); + + // When decoding the properties from the stream using a flog that does not allow duplicates + celix_autoptr(celix_properties_t) props2 = nullptr; + status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); + + // Then loading fails, because of a duplicate key + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); +} + +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesEscapedSlashesTest) { + // Given a complex JSON object with collisions and duplicate keys + // Collisions: + // - object object1/object2 and value object1/object2 + // - value key1 in object2 in object1 and value object2/key in object1 + // - value object3/key4 and value key4 in object object3 + // Duplicate JSON keys: + // - key3 const char* jsonInput = R"({ "object1": { "object2": { - "key1":true - } + "key1": "value1" + }, + "object2/key2": "value2" }, - "object1/object2/key1":6, - "key2":2, - "key2":3 + "object1/object2" : "value3", + "key3": "value4", + "key3": "value5", + "object3/key4": "value6", + "object3": { + "key4": "value7" + } })"; - // And a stream with the JSON object - FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - - // When decoding the properties from the stream + // When loading the properties from a string. celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromString2(jsonInput, 0, &props); // Then loading succeeds ASSERT_EQ(CELIX_SUCCESS, status); - // And the properties object contains the last values of the jpath keys - EXPECT_EQ(2, celix_properties_size(props)); - EXPECT_EQ(6, celix_properties_getLong(props, "object1/object2/key1", 0)); - EXPECT_EQ(3, celix_properties_getLong(props, "key2", 0)); - - // When the stream is reset - fseek(stream, 0, SEEK_SET); + // And the properties object all the values for the colliding keys and a single (latest) value for the duplicate + // keys + EXPECT_EQ(5, celix_properties_size(props)); + EXPECT_STREQ("value1", celix_properties_getString(props, "object1/object2/key1")); + EXPECT_STREQ("value2", celix_properties_getString(props, "object1/object2/key2")); + EXPECT_STREQ("value3", celix_properties_getString(props, "object1/object2")); + EXPECT_STREQ("value5", celix_properties_getString(props, "key3")); + EXPECT_STREQ("value7", celix_properties_getString(props, "object3/key4")); - // And decoding the properties from the stream using a flog that does not allow collisions + // When decoding the properties from a string using a flag that allows duplicates celix_autoptr(celix_properties_t) props2 = nullptr; - status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); + status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2); // Then loading fails, because of a duplicate key EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -551,9 +698,20 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa // And at least one error message is added to celix_err EXPECT_GE(celix_err_getErrorCount(), 1); celix_err_printErrors(stderr, "Error: ", "\n"); + + // When decoding the properties from a string using a flag that allows collisions + celix_autoptr(celix_properties_t) props3 = nullptr; + status = celix_properties_loadFromString2(jsonInput, CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS, &props3); + + // Then loading fails, because of a collision + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + // And at least one error message is added to celix_err + EXPECT_GE(celix_err_getErrorCount(), 1); + celix_err_printErrors(stderr, "Error: ", "\n"); } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabledTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithAndWithoutStrictFlagTest) { auto invalidInputs = { R"({"mixedArr":["string", true]})", // Mixed array gives error on strict R"({"key1":null})", // Null value gives error on strict @@ -567,9 +725,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabl //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When decoding the properties from the stream with an empty flags + //When loading the properties from the stream with an empty flags celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); celix_err_printErrors(stderr, "Error: ", "\n"); //Then decoding succeeds, because strict is disabled @@ -587,9 +745,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabl //Given an invalid JSON object FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); - //When decoding the properties from the stream with a strict flag + //When loading the properties from the stream with a strict flag celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); + auto status = celix_properties_loadFromStream(stream, CELIX_PROPERTIES_DECODE_STRICT, &props); //Then decoding fails EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); @@ -602,7 +760,7 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithStrictEnabledDisabl } } -TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest) { +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSlashesInTheKeysTest) { // Given a complex JSON object const char* jsonInput = R"({ "/": "value1", @@ -618,9 +776,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest // And a stream with the JSON object FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); - // When decoding the properties from the stream + // When loading the properties from the stream celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_decodeFromStream(stream, 0, &props); + auto status = celix_properties_loadFromStream(stream, 0, &props); celix_err_printErrors(stderr, "Error: ", "\n"); ASSERT_EQ(CELIX_SUCCESS, status); @@ -636,5 +794,6 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest //TODO test with invalid version string //TODO is there a strict option needed for version (e.g. not parseable as version handle as string) -//TODO test encoding flags -//TODO error injection tests and wrappers for jansson functions \ No newline at end of file +//TODO error injection tests and wrappers for jansson functions +//TODO test load and save with filename +//TODO check all the celix_err messages for consistency diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h index b82ad9ad..04dd04f9 100644 --- a/libs/utils/include/celix_properties.h +++ b/libs/utils/include/celix_properties.h @@ -180,31 +180,6 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro const char* file, const char* header); -//TODO document the encode flags -#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 -#define CELIX_PROPERTIES_ENCODE_SORT_KEYS 0x02 - -//TODO doc. Not encode does not reset the stream position. -CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, - FILE* stream, - int encodeFlags); - -//TODO document the decode flags -#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x02 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x04 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x04 -#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x08 -#define CELIX_PROPERTIES_DECODE_STRICT \ - (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | \ - CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | \ - CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) - -//TODO doc. Note decode does not reset the stream position. -CELIX_UTILS_EXPORT celix_status_t celix_properties_decodeFromStream(FILE* stream, - int decodeFlags, - celix_properties_t** out); - /** * @brief Get the entry for a given key in a property set. * @@ -962,6 +937,62 @@ CELIX_UTILS_EXPORT bool celix_propertiesIterator_equals(const celix_properties_i !celix_propertiesIterator_isEnd(&(iterName)); \ celix_propertiesIterator_next(&(iterName))) + + +//TODO document the encode flags +#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01 +#define CELIX_PROPERTIES_ENCODE_FLAT 0x02 //TODO doc, explain that decoding options ensures all properties entries are written, but only as a top level field entries. +#define CELIX_PROPERTIES_ENCODE_NESTED 0x04 + +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS 0x10 +#define CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS 0x20 +#define CELIX_PROPERTIES_ENCODE_STRICT \ + (CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS | CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) + +//TODO doc +CELIX_UTILS_EXPORT celix_status_t celix_properties_save(const celix_properties_t* properties, + const char* filename, + int encodeFlags); + +//TODO doc. Not encode does not reset or close the stream position. +CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, + FILE* stream, + int encodeFlags); + +//TODO doc +CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToString(const celix_properties_t* properties, + int encodeFlags, + char** out); + + +//TODO document the decode flags +#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS 0x02 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x04 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x08 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS 0x10 +#define CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS 0x20 +#define CELIX_PROPERTIES_DECODE_STRICT \ + (CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES | CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS | \ + CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES | CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS | \ + CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS | CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS) + +//TODO doc. Note load2, because load is currently already used. Will be updated in the future. +CELIX_UTILS_EXPORT celix_status_t celix_properties_load2(const char* filename, + int decodeFlags, + celix_properties_t** out); + +//TODO doc. Note decode does not reset or close the stream position. +CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, + int decodeFlags, + celix_properties_t** out); + +//TODO doc. Note celix_properties_loadFromString2, because loadFromString is currently already used. Will be updated in the future. +CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromString2(const char* input, + int decodeFlags, + celix_properties_t** out); + + #ifdef __cplusplus } #endif diff --git a/libs/utils/include/celix_utils.h b/libs/utils/include/celix_utils.h index 7933b4eb..6f58f2b7 100644 --- a/libs/utils/include/celix_utils.h +++ b/libs/utils/include/celix_utils.h @@ -29,6 +29,8 @@ extern "C" { #include <stdbool.h> #include "celix_utils_export.h" +#include "celix_compiler.h" +#include "celix_cleanup.h" #define CELIX_US_IN_SEC (1000000) #define CELIX_NS_IN_SEC ((CELIX_US_IN_SEC)*1000) @@ -91,6 +93,60 @@ __attribute__((format(printf, 3, 0))); */ CELIX_UTILS_EXPORT void celix_utils_freeStringIfNotEqual(const char* buffer, char* str); +/** + * @brief Guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString. + * + * Can be used with celix_auto() to automatically and correctly free the string. + * If the string is pointing to the buffer, the string should be freed, otherwise the string should be freed. + * + * + */ +typedef struct celix_utils_string_guard { + const char* buffer; + char* string; +} celix_utils_string_guard_t; + +/** + * @brief Initialize a guard for a string created with celix_utils_writeOrCreateString, celix_utils_writeOrCreateVString. + * + * De-initialize with celix_utils_stringGuard_deinit(). + * + * No allocation is performed. + * This is intended to be used with celix_auto(). + * + * * Example: +* ``` + * const char* possibleLongString = ... + * char buffer[64]; + * char* str = celix_utils_writeOrCreateString(buffer, sizeof(buffer), "Hello %s", possibleLongString); + * celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buffer, str); + * ``` + * If the strGuard goes out of scope, the string will be freed correctly. + * + * @param buffer A (local) buffer which was potentially used to create the string. + * @param string The string to guard. + * @return An initialized string guard to be used with celix_auto(). + */ +static CELIX_UNUSED inline celix_utils_string_guard_t celix_utils_stringGuard_init(const char* buffer, char* string) { + celix_utils_string_guard_t guard; + guard.buffer = buffer; + guard.string = string; + return guard; +} + +/** + * @brief De-initialize a string guard. + * + * This will free the string if it is not equal to the buffer. + * This is intended to be used with celix_auto(). + * + * @param guard The guard to de-initialize. + */ +static CELIX_UNUSED inline void celix_utils_stringGuard_deinit(celix_utils_string_guard_t* guard) { + celix_utils_freeStringIfNotEqual(guard->buffer, guard->string); +} + +CELIX_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(celix_utils_string_guard_t, celix_utils_stringGuard_deinit) /** * @brief Compares two strings and returns true if the strings are equal. diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c index ba30b670..e1ad99b0 100644 --- a/libs/utils/src/properties_encoding.c +++ b/libs/utils/src/properties_encoding.c @@ -31,8 +31,6 @@ static celix_status_t celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags); -// TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection - static celix_status_t celix_properties_versionToJson(const celix_version_t* version, json_t** out) { celix_autofree char* versionStr = celix_version_toString(version); if (!versionStr) { @@ -68,7 +66,7 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ return celix_properties_versionToJson(entry.versionVal, out); default: // LCOV_EXCL_START - celix_err_pushf("Unexpected array list element type %d", elType); + celix_err_pushf("Invalid array list element type %d", elType); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -79,9 +77,16 @@ static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_ return CELIX_SUCCESS; } -static celix_status_t celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry, json_t** out) { +static celix_status_t celix_properties_arrayEntryValueToJson(const char* key, + const celix_properties_entry_t* entry, + int flags, + json_t** out) { *out = NULL; if (celix_arrayList_size(entry->typed.arrayValue) == 0) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_EMPTY_ARRAYS) { + celix_err_pushf("Invalid empty array for key %s", key); + return CELIX_ILLEGAL_ARGUMENT; + } return CELIX_SUCCESS; // empty array -> treat as unset property } @@ -115,7 +120,8 @@ static celix_status_t celix_properties_arrayEntryValueToJson(const celix_propert return CELIX_SUCCESS; } -static celix_status_t celix_properties_entryValueToJson(const celix_properties_entry_t* entry, json_t** out) { +static celix_status_t +celix_properties_entryValueToJson(const char* key, const celix_properties_entry_t* entry, int flags, json_t** out) { *out = NULL; switch (entry->valueType) { case CELIX_PROPERTIES_VALUE_TYPE_STRING: @@ -126,7 +132,7 @@ static celix_status_t celix_properties_entryValueToJson(const celix_properties_e break; case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: if (isnan(entry->typed.doubleValue) || isinf(entry->typed.doubleValue)) { - celix_err_pushf("Double NaN or Inf not supported in JSON."); + celix_err_pushf("Invalid NaN or Inf in key '%s'.", key); return CELIX_ILLEGAL_ARGUMENT; } *out = json_real(entry->typed.doubleValue); @@ -137,34 +143,76 @@ static celix_status_t celix_properties_entryValueToJson(const celix_properties_e case CELIX_PROPERTIES_VALUE_TYPE_VERSION: return celix_properties_versionToJson(entry->typed.versionValue, out); case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST: - return celix_properties_arrayEntryValueToJson(entry, out); + return celix_properties_arrayEntryValueToJson(key, entry, flags, out); default: // LCOV_EXCL_START - celix_err_pushf("Unexpected properties entry type %d", entry->valueType); + celix_err_pushf("Invalid properties entry type %d.", entry->valueType); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } if (!*out) { - celix_err_push("Failed to create json value"); + celix_err_pushf("Failed to create json value for key '%s'.", key); return CELIX_ENOMEM; } return CELIX_SUCCESS; } -static celix_status_t -celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const char* key, json_t* root) { +static celix_status_t celix_properties_addJsonValueToJson(json_t* value, const char* key, json_t* obj, int flags) { + if (!value) { + // ignore unset values + return CELIX_SUCCESS; + } + + json_t* field = json_object_get(obj, key); + if (field) { + if (flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { + celix_err_pushf("Invalid key collision. key '%s' already exists.", key); + return CELIX_ILLEGAL_ARGUMENT; + } + } + + int rc = json_object_set_new(obj, key, value); + if (rc != 0) { + celix_err_push("Failed to set json object"); + return CELIX_ENOMEM; + } + return CELIX_SUCCESS; +} + +static celix_status_t celix_properties_addPropertiesEntryFlatToJson(const celix_properties_entry_t* entry, + const char* key, + json_t* root, + int flags) { + json_t* value; + celix_status_t status = celix_properties_entryValueToJson(key, entry, flags, &value); + if (status != CELIX_SUCCESS) { + return status; + } + return celix_properties_addJsonValueToJson(value, key, root, flags); +} + +static celix_status_t celix_properties_addPropertiesEntryToJson(const celix_properties_entry_t* entry, + const char* key, + json_t* root, + int flags) { json_t* jsonObj = root; - const char* subKey = key; + const char* fieldName = key; const char* slash = strstr(key, "/"); while (slash) { - celix_autofree const char* name = strndup(subKey, slash - subKey); + char buf[64]; + char* name = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)(slash - fieldName), fieldName); + celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, name); if (!name) { celix_err_push("Failed to create name string"); return CELIX_ENOMEM; } json_t* subObj = json_object_get(jsonObj, name); - if (!subObj) { + if (!subObj || !json_is_object(subObj)) { + if (!json_is_object(subObj) && flags & CELIX_PROPERTIES_ENCODE_ERROR_ON_COLLISIONS) { + celix_err_pushf("Invalid key collision. Key '%s' already exists.", name); + return CELIX_ILLEGAL_ARGUMENT; + } subObj = json_object(); if (!subObj) { celix_err_push("Failed to create json object"); @@ -175,51 +223,40 @@ celix_properties_addEntryToJson(const celix_properties_entry_t* entry, const cha celix_err_push("Failed to set json object"); return CELIX_ENOMEM; } - } else if (!json_is_object(subObj)) { - // subObj is not an object, so obj cannot be added -> adding obj flat - jsonObj = root; - subKey = key; - break; } jsonObj = subObj; - subKey = slash + 1; - slash = strstr(subKey, "/"); - - json_t* field = json_object_get(jsonObj, subKey); - if (field) { - // field already exists, so adding obj flat - jsonObj = root; - subKey = key; - break; - } + fieldName = slash + 1; + slash = strstr(fieldName, "/"); } json_t* value; - celix_status_t status = celix_properties_entryValueToJson(entry, &value); + celix_status_t status = celix_properties_entryValueToJson(fieldName, entry, flags, &value); if (status != CELIX_SUCCESS) { return status; - } else if (!value) { - // ignore unset values - } else { - int rc = json_object_set_new(jsonObj, subKey, value); - if (rc != 0) { - celix_err_push("Failed to set json object"); - return CELIX_ENOMEM; - } } - - return CELIX_SUCCESS; + return celix_properties_addJsonValueToJson(value, fieldName, jsonObj, flags); } -celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) { +celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream, int encodeFlags) { json_t* root = json_object(); if (!root) { celix_err_push("Failed to create json object"); } + if (!(encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) && !(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED)) { + //no encoding flags set, default to flat + encodeFlags |= CELIX_PROPERTIES_ENCODE_FLAT; + } + CELIX_PROPERTIES_ITERATE(properties, iter) { - celix_status_t status = celix_properties_addEntryToJson(&iter.entry, iter.key, root); + celix_status_t status; + if (encodeFlags & CELIX_PROPERTIES_ENCODE_FLAT) { + status = celix_properties_addPropertiesEntryFlatToJson(&iter.entry, iter.key, root, encodeFlags); + } else { + assert(encodeFlags & CELIX_PROPERTIES_ENCODE_NESTED); + status = celix_properties_addPropertiesEntryToJson(&iter.entry, iter.key, root, encodeFlags); + } if (status != CELIX_SUCCESS) { json_decref(root); return status; @@ -230,9 +267,6 @@ celix_status_t celix_properties_encodeToStream(const celix_properties_t* propert if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) { jsonFlags = JSON_INDENT(2); } - if (encodeFlags & CELIX_PROPERTIES_ENCODE_SORT_KEYS) { - jsonFlags |= JSON_SORT_KEYS; - } int rc = json_dumpf(root, stream, jsonFlags); json_decref(root); @@ -243,6 +277,35 @@ celix_status_t celix_properties_encodeToStream(const celix_properties_t* propert return CELIX_SUCCESS; } +celix_status_t celix_properties_save(const celix_properties_t* properties, const char* filename, int encodeFlags) { + FILE* stream = fopen(filename, "w"); + if (!stream) { + celix_err_pushf("Failed to open file %s", filename); + return CELIX_FILE_IO_EXCEPTION; + } + celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); + fclose(stream); + return status; +} + +celix_status_t celix_properties_saveToString(const celix_properties_t* properties, int encodeFlags, char** out) { + *out = NULL; + celix_autofree char* buffer = NULL; + size_t size = 0; + FILE* stream = open_memstream(&buffer, &size); + if (!stream) { + celix_err_push("Failed to open memstream"); + return CELIX_FILE_IO_EXCEPTION; + } + + celix_status_t status = celix_properties_saveToStream(properties, stream, encodeFlags); + fclose(stream); + if (status == CELIX_SUCCESS) { + *out = celix_steal_ptr(buffer); + } + return status; +} + static celix_version_t* celix_properties_parseVersion(const char* value) { // precondition: value is a valid version string (14 chars prefix and 1 char suffix) celix_version_t* version = NULL; @@ -326,7 +389,7 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra break; case JSON_NULL: default: - return CELIX_ILLEGAL_ARGUMENT; // TODO Add test for this case and maybe return a different error code and log error + return CELIX_ILLEGAL_ARGUMENT; } return CELIX_SUCCESS; @@ -338,7 +401,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) { celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY); - celix_err_pushf("Unsupported mixed or null array for key '%s': %s", key, arrStr); + celix_err_pushf("Invalid mixed or null array for key '%s': %s", key, arrStr); return status; } else if (status != CELIX_SUCCESS) { //ignore mixed types @@ -378,7 +441,7 @@ celix_properties_decodeArray(celix_properties_t* props, const char* key, const j } default: // LCOV_EXCL_START - celix_err_pushf("Unexpected array list element type %d for key %s", elType, key); + celix_err_pushf("Invalid array list element type %d for key %s", elType, key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -399,8 +462,9 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* return CELIX_SUCCESS; // ignore empty keys. } - if (celix_properties_hasKey(props, key) && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES)) { - celix_err_pushf("Key `%s` already exists.", key); + if (!json_is_object(jsonValue) && celix_properties_hasKey(props, key) && + (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_COLLISIONS)) { + celix_err_pushf("Invalid key collision. Key '%s' already exists.", key); return CELIX_ILLEGAL_ARGUMENT; } @@ -423,13 +487,14 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* const char* fieldName; json_t* fieldValue; json_object_foreach(jsonValue, fieldName, fieldValue) { - celix_autofree char* subKey; - int rc = asprintf(&subKey, "%s/%s", key, fieldName); - if (rc < 0) { - celix_err_push("Failed to create sub key"); - return CELIX_ENOMEM; + char buf[64]; + char* combinedKey = celix_utils_writeOrCreateString(buf, sizeof(buf), "%s/%s", key, fieldName); + celix_auto(celix_utils_string_guard_t) strGuard = celix_utils_stringGuard_init(buf, combinedKey); + if (!combinedKey) { + celix_err_push("Failed to create sub key"); + return CELIX_ENOMEM; } - status = celix_properties_decodeValue(props, subKey, fieldValue, flags); + status = celix_properties_decodeValue(props, combinedKey, fieldValue, flags); if (status != CELIX_SUCCESS) { return status; } @@ -437,7 +502,7 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* return CELIX_SUCCESS; } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) { - celix_err_pushf("Unexpected empty array for key `%s`", key); + celix_err_pushf("Invalid empty array for key '%s'", key); return CELIX_ILLEGAL_ARGUMENT; } // ignore empty arrays @@ -446,14 +511,14 @@ celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* status = celix_properties_decodeArray(props, key, jsonValue, flags); } else if (json_is_null(jsonValue)) { if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) { - celix_err_pushf("Unexpected null value for key `%s`", key); + celix_err_pushf("Invalid null value for key '%s'", key); return CELIX_ILLEGAL_ARGUMENT; } // ignore null values return CELIX_SUCCESS; } else { // LCOV_EXCL_START - celix_err_pushf("Unexpected json value type for key `%s`", key); + celix_err_pushf("Invalid json value type for key '%s'", key); return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } @@ -484,7 +549,7 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, ce return CELIX_SUCCESS; } -celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) { +celix_status_t celix_properties_loadFromStream(FILE* stream, int decodeFlags, celix_properties_t** out) { json_error_t jsonError; size_t jsonFlags = 0; if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) { @@ -495,6 +560,27 @@ celix_status_t celix_properties_decodeFromStream(FILE* stream, int decodeFlags, celix_err_pushf("Failed to parse json: %s", jsonError.text); return CELIX_ILLEGAL_ARGUMENT; } - return celix_properties_decodeFromJson(root, decodeFlags, out); } + +celix_status_t celix_properties_load2(const char* filename, int decodeFlags, celix_properties_t** out) { + FILE* stream = fopen(filename, "r"); + if (!stream) { + celix_err_pushf("Failed to open file %s", filename); + return CELIX_FILE_IO_EXCEPTION; + } + celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); + fclose(stream); + return status; +} + +celix_status_t celix_properties_loadFromString2(const char* input, int decodeFlags, celix_properties_t** out) { + FILE* stream = fmemopen((void*)input, strlen(input), "r"); + if (!stream) { + celix_err_push("Failed to open memstream"); + return CELIX_FILE_IO_EXCEPTION; + } + celix_status_t status = celix_properties_loadFromStream(stream, decodeFlags, out); + fclose(stream); + return status; +}