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 de0725649450bcbcfbc8145d652b7fb0ea6f0716 Author: Pepijn Noltes <[email protected]> AuthorDate: Mon Apr 8 23:03:03 2024 +0200 gh-685: Add json prop loading for primitive and initial arr. --- .../gtest/src/PropertiesSerializationTestSuite.cc | 311 ++++++++++++++- libs/utils/src/properties_serialization.c | 420 ++++++++++++++++++--- 2 files changed, 653 insertions(+), 78 deletions(-) diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc index 99cbeceb..a5d6a1df 100644 --- a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc +++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc @@ -17,6 +17,7 @@ * under the License. */ +#include <cmath> #include <gtest/gtest.h> #include <jansson.h> @@ -49,23 +50,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) { EXPECT_STREQ("{}", buf); } -TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { - //Given an empty JSON object - const char* json = "{}"; - FILE* stream = fmemopen((void*)json, strlen(json), "r"); - - //When loading the properties from the stream - celix_autoptr(celix_properties_t) props = nullptr; - auto status = celix_properties_loadFromStream(stream, &props); - EXPECT_EQ(CELIX_SUCCESS, status); - - //Then the properties object is empty - EXPECT_EQ(0, celix_properties_size(props)); - - fclose(stream); -} - -TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingelValuesTest) { +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"); @@ -102,14 +87,115 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingelValuesTest) { json_decref(root); } +TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithNaNAndInfValuesTest) { + //Given a NAN, INF and -INF value + auto keys = {"NAN", "INF", "-INF"}; + for (const auto& key : keys) { + //For every value + + //Given a properties object with a NAN, INF or -INF value + celix_autoptr(celix_properties_t) props = celix_properties_create(); + celix_properties_setDouble(props, key, strtod(key, nullptr)); + + //And an in-memory stream + celix_autofree char* buf = nullptr; + size_t bufLen = 0; + FILE* stream = open_memstream(&buf, &bufLen); + + //Then saving the properties to the stream fails, because JSON does not support NAN, INF and -INF + celix_err_resetErrors(); + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status); + + //And an error msg is added to celix_err + EXPECT_EQ(1, celix_err_getErrorCount()); + } +} + + TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { - //Given a properties object with array list values + // 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(); celix_arrayList_addString(list1, "value1"); celix_arrayList_addString(list1, "value2"); celix_properties_assignArrayList(props, "key1", list1); - //TODO long, double, bool, version + celix_array_list_t* list2 = celix_arrayList_createLongArray(); + celix_arrayList_addLong(list2, 1); + celix_arrayList_addLong(list2, 2); + celix_properties_assignArrayList(props, "key2", list2); + celix_array_list_t* list3 = celix_arrayList_createDoubleArray(); + celix_arrayList_addDouble(list3, 1.0); + celix_arrayList_addDouble(list3, 2.0); + celix_properties_assignArrayList(props, "key3", list3); + celix_array_list_t* list4 = celix_arrayList_createBoolArray(); + celix_arrayList_addBool(list4, true); + celix_arrayList_addBool(list4, false); + celix_properties_assignArrayList(props, "key4", list4); + celix_array_list_t* list5 = celix_arrayList_createVersionArray(); + celix_arrayList_addVersion(list5, celix_version_create(1, 2, 3, "qualifier")); + celix_arrayList_addVersion(list5, celix_version_create(4, 5, 6, "qualifier")); + celix_properties_assignArrayList(props, "key5", list5); + + // 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 the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + // Then the stream contains the JSON representation snippets of the properties + fclose(stream); + EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key2":[1,2])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key3":[1.0,2.0])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key4":[true,false])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key5":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"])")) + << "JSON: " << buf; + + // 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, 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()); + celix_properties_assignArrayList(props, "key2", celix_arrayList_createLongArray()); + celix_properties_assignArrayList(props, "key3", celix_arrayList_createDoubleArray()); + celix_properties_assignArrayList(props, "key4", celix_arrayList_createBoolArray()); + 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 the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the stream contains an empty JSON object, because empty arrays are treated as unset + fclose(stream); + EXPECT_STREQ("{}", buf); +} + +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"); + celix_properties_set(props, "key2", "value2"); + celix_properties_set(props, "object1/key3", "value3"); + celix_properties_set(props, "object1/key4", "value4"); + celix_properties_set(props, "object2/key5", "value5"); + celix_properties_set(props, "object3/object4/key6", "value6"); //And an in-memory stream celix_autofree char* buf = nullptr; @@ -122,7 +208,44 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { //Then the stream contains the JSON representation snippets of the properties fclose(stream); - EXPECT_NE(nullptr, strstr(buf, R"("key1":["value1","value2"])")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key1":"value1")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("object1":{"key3":"value3","key4":"value4"})")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("object2":{"key5":"value5"})")) << "JSON: " << buf; + EXPECT_NE(nullptr, strstr(buf, R"("object3":{"object4":{"key6":"value6"}})")) << "JSON: " << buf; + + //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, SaveJPathKeysWithCollisionTest) { + //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" + + //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 the stream + auto status = celix_properties_saveToStream(props, stream); + EXPECT_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. //And the buf is a valid JSON object json_error_t error; @@ -130,3 +253,151 @@ TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) { EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text; json_decref(root); } + +TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) { + //Given an empty JSON object + const char* json = "{}"; + FILE* stream = fmemopen((void*)json, strlen(json), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object is empty + EXPECT_EQ(0, celix_properties_size(props)); + + fclose(stream); +} + +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", + "longKey":42, + "doubleKey":2.0, + "boolKey":true, + "versionKey":"celix_version<1.2.3.qualifier>" + })"; + + //And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object contains the single values + EXPECT_EQ(5, celix_properties_size(props)); + EXPECT_STREQ("strValue", celix_properties_getString(props, "strKey")); + EXPECT_EQ(42, celix_properties_getLong(props, "longKey", -1)); + EXPECT_DOUBLE_EQ(2.0, celix_properties_getDouble(props, "doubleKey", NAN)); + EXPECT_TRUE(celix_properties_getBool(props, "boolKey", false)); + auto* v = celix_properties_getVersion(props, "versionKey"); + ASSERT_NE(nullptr, v); + celix_autofree char* vStr = celix_version_toString(v); + EXPECT_STREQ("1.2.3.qualifier", vStr); +} + +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"], + "intArr":[1,2], + "realArr":[1.0,2.0], + "boolArr":[true,false], + "versionArr":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"] + })"; + + //And a stream with the JSON object + FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + EXPECT_EQ(CELIX_SUCCESS, status); + + //Then the properties object contains the array values + EXPECT_EQ(5, celix_properties_size(props)); + + //And the string array is correctly loaded + auto* strArr = celix_properties_getArrayList(props, "strArr"); + ASSERT_NE(nullptr, strArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING, celix_arrayList_getElementType(strArr)); + EXPECT_EQ(2, celix_arrayList_size(strArr)); + EXPECT_STREQ("value1", celix_arrayList_getString(strArr, 0)); + EXPECT_STREQ("value2", celix_arrayList_getString(strArr, 1)); + + //And the long array is correctly loaded + auto* intArr = celix_properties_getArrayList(props, "intArr"); + ASSERT_NE(nullptr, intArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG, celix_arrayList_getElementType(intArr)); + EXPECT_EQ(2, celix_arrayList_size(intArr)); + EXPECT_EQ(1, celix_arrayList_getLong(intArr, 0)); + EXPECT_EQ(2, celix_arrayList_getLong(intArr, 1)); + + //And the double array is correctly loaded + auto* realArr = celix_properties_getArrayList(props, "realArr"); + ASSERT_NE(nullptr, realArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, celix_arrayList_getElementType(realArr)); + EXPECT_EQ(2, celix_arrayList_size(realArr)); + EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(realArr, 0)); + EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(realArr, 1)); + + //And the bool array is correctly loaded + auto* boolArr = celix_properties_getArrayList(props, "boolArr"); + ASSERT_NE(nullptr, boolArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL, celix_arrayList_getElementType(boolArr)); + EXPECT_EQ(2, celix_arrayList_size(boolArr)); + EXPECT_TRUE(celix_arrayList_getBool(boolArr, 0)); + EXPECT_FALSE(celix_arrayList_getBool(boolArr, 1)); + + //And the version array is correctly loaded + auto* versionArr = celix_properties_getArrayList(props, "versionArr"); + ASSERT_NE(nullptr, versionArr); + EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION, celix_arrayList_getElementType(versionArr)); + EXPECT_EQ(2, celix_arrayList_size(versionArr)); + auto* v1 = celix_arrayList_getVersion(versionArr, 0); + ASSERT_NE(nullptr, v1); + celix_autofree char* v1Str = celix_version_toString(v1); + EXPECT_STREQ("1.2.3.qualifier", v1Str); + auto* v2 = celix_arrayList_getVersion(versionArr, 1); + ASSERT_NE(nullptr, v2); + celix_autofree char* v2Str = celix_version_toString(v2); + EXPECT_STREQ("4.5.6.qualifier", v2Str); +} + +//TODO test with combination json_int and json_real, this should be promoted to double + +TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) { + auto invalidInputs = { + R"({)", // invalid JSON (caught by jansson) + R"({"emptyArr":[]})", // Empty array, not supported + R"({"mixedArr":["string", true]})", // Mixed array, not supported + R"({"mixedArr":[1.9, 2]})", // Mixed array, TODO this should be supported + }; + for (auto& invalidInput: invalidInputs) { + //Given an invalid JSON object + FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), "r"); + + //When loading the properties from the stream + celix_autoptr(celix_properties_t) props = nullptr; + auto status = celix_properties_loadFromStream(stream, &props); + + //Then loading fails + 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"); + + fclose(stream); + } +} + +//TODO test deserialize null values +//TODO test serialize with empty array (treated as unset) +//TODO test with jpath subset keys and json serialization +//TODO test with key starting and ending with slash diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_serialization.c index d52fa6ef..0b4c5f8b 100644 --- a/libs/utils/src/properties_serialization.c +++ b/libs/utils/src/properties_serialization.c @@ -21,83 +21,194 @@ #include "celix_err.h" #include "celix_stdlib_cleanup.h" +#include "celix_utils.h" +#include <assert.h> #include <jansson.h> +#include <math.h> +#include <string.h> -//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_loadValue(celix_properties_t* props, const char* key, const json_t* jsonValue); -static json_t* celix_properties_versionToJson(const celix_version_t *version) { - celix_autofree char* versionStr = celix_version_toString(version); // TODO error handling - return json_sprintf("celix_version<%s>", versionStr); +// 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) { + celix_err_push("Failed to create version string"); + return CELIX_ENOMEM; + } + *out = json_sprintf("celix_version<%s>", versionStr); + if (!*out) { + celix_err_push("Failed to create json string"); + return CELIX_ENOMEM; + } + return CELIX_SUCCESS; } -static json_t* celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType, - celix_array_list_entry_t entry) { +static celix_status_t celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType, + celix_array_list_entry_t entry, + json_t** out) { + *out = NULL; switch (elType) { case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING: - return json_string(entry.stringVal); + *out = json_string(entry.stringVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG: - return json_integer(entry.longVal); + *out = json_integer(entry.longVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: - return json_real(entry.doubleVal); + *out = json_real(entry.doubleVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: - return json_boolean(entry.boolVal); + *out = json_boolean(entry.boolVal); + break; case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: - celix_properties_versionToJson(entry.versionVal); + return celix_properties_versionToJson(entry.versionVal, out); default: // LCOV_EXCL_START celix_err_pushf("Unexpected array list element type %d", elType); - return NULL; + return CELIX_ILLEGAL_ARGUMENT; // LCOV_EXCL_STOP } + if (!*out) { + celix_err_push("Failed to create json value"); + return CELIX_ENOMEM; + } + return CELIX_SUCCESS; } -static json_t* celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry) { +static celix_status_t celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry, json_t** out) { + *out = NULL; + if (celix_arrayList_size(entry->typed.arrayValue) == 0) { + return CELIX_SUCCESS; // empty array -> treat as unset property + } + json_t* array = json_array(); if (!array) { celix_err_push("Failed to create json array"); - return NULL; + return CELIX_ENOMEM; } for (int i = 0; i < celix_arrayList_size(entry->typed.arrayValue); ++i) { celix_array_list_entry_t arrayEntry = celix_arrayList_getEntry(entry->typed.arrayValue, i); celix_array_list_element_type_t elType = celix_arrayList_getElementType(entry->typed.arrayValue); - json_t* jsonValue = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry); - if (!jsonValue) { - celix_err_push("Failed to create json string"); - json_decref(array); - return NULL; - } - int rc = json_array_append_new(array, jsonValue); - if (rc != 0) { - celix_err_push("Failed to append json string to array"); + json_t* jsonValue; + celix_status_t status = celix_properties_arrayElementEntryValueToJson(elType, arrayEntry, &jsonValue); + if (status != CELIX_SUCCESS) { json_decref(array); - return NULL; + return status; + } else if (!jsonValue) { + // ignore unset values + } else { + int rc = json_array_append_new(array, jsonValue); + if (rc != 0) { + celix_err_push("Failed to append json string to array"); + json_decref(array); + return CELIX_ENOMEM; + } } } - return array; + + *out = array; + return CELIX_SUCCESS; } -static json_t* celix_properties_entryValueToJson(const celix_properties_entry_t* entry) { +static celix_status_t celix_properties_entryValueToJson(const celix_properties_entry_t* entry, json_t** out) { + *out = NULL; switch (entry->valueType) { - case CELIX_PROPERTIES_VALUE_TYPE_STRING: - return json_string(entry->value); - case CELIX_PROPERTIES_VALUE_TYPE_LONG: - return json_integer(entry->typed.longValue); - case CELIX_PROPERTIES_VALUE_TYPE_DOUBLE: - return json_real(entry->typed.doubleValue); - case CELIX_PROPERTIES_VALUE_TYPE_BOOL: - return json_boolean(entry->typed.boolValue); - case CELIX_PROPERTIES_VALUE_TYPE_VERSION: - return celix_properties_versionToJson(entry->typed.versionValue); - case CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST: - return celix_properties_arrayEntryValueToJson(entry); - default: - //LCOV_EXCL_START - celix_err_pushf("Unexpected properties entry type %d", entry->valueType);\ - return NULL; - //LCOV_EXCL_STOP + case CELIX_PROPERTIES_VALUE_TYPE_STRING: + *out = json_string(entry->value); + break; + case CELIX_PROPERTIES_VALUE_TYPE_LONG: + *out = json_integer(entry->typed.longValue); + 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."); + return CELIX_ILLEGAL_ARGUMENT; + } + *out = json_real(entry->typed.doubleValue); + break; + case CELIX_PROPERTIES_VALUE_TYPE_BOOL: + *out = json_boolean(entry->typed.boolValue); + break; + 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); + default: + // LCOV_EXCL_START + celix_err_pushf("Unexpected properties entry type %d", entry->valueType); + return CELIX_ILLEGAL_ARGUMENT; + // LCOV_EXCL_STOP + } + + if (!*out) { + celix_err_push("Failed to create json value"); + 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) { + json_t* jsonObj = root; + const char* subKey = key; + const char* slash = strstr(key, "/"); + while (slash) { + celix_autofree const char* name = strndup(subKey, slash - subKey); + if (!name) { + celix_err_push("Failed to create name string"); + return CELIX_ENOMEM; + } + json_t* subObj = json_object_get(jsonObj, name); + if (!subObj) { + subObj = json_object(); + if (!subObj) { + celix_err_push("Failed to create json object"); + return CELIX_ENOMEM; + } + int rc = json_object_set_new(jsonObj, name, subObj); + if (rc != 0) { + 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; + } + } + + json_t* value; + celix_status_t status = celix_properties_entryValueToJson(entry, &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; } celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) { @@ -107,33 +218,226 @@ celix_status_t celix_properties_saveToStream(const celix_properties_t* propertie } CELIX_PROPERTIES_ITERATE(properties, iter) { - const char* key = iter.key; - json_t* value = celix_properties_entryValueToJson(&iter.entry); - if (!value) { - json_decref(root); - return CELIX_ENOMEM; //TODO improve error - } - int rc = json_object_set_new(root, key, value); - if (rc != 0) { - celix_err_push("Failed to set json object"); + celix_status_t status = celix_properties_addEntryToJson(&iter.entry, iter.key, root); + if (status != CELIX_SUCCESS) { json_decref(root); - return CELIX_ENOMEM; //TODO improve error + return status; } } - int rc = json_dumpf(root, stream, JSON_COMPACT); //TODO make celix properties flags for COMPACT and INDENT and maybe other json flags + int rc = + json_dumpf(root, + stream, + JSON_COMPACT); // TODO make celix properties flags for COMPACT and INDENT and maybe other json flags json_decref(root); if (rc != 0) { celix_err_push("Failed to dump json object"); - return CELIX_ENOMEM; //TODO improve error + return CELIX_ENOMEM; // TODO improve error } return CELIX_SUCCESS; } -celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { +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; + char buf[32]; + char* corrected = celix_utils_writeOrCreateString(buf, sizeof(buf), "%.*s", (int)strlen(value) - 15, value + 14); + if (!corrected) { + celix_err_push("Failed to create corrected version string"); + return NULL; + } + celix_status_t status = celix_version_parse(corrected, &version); + celix_utils_freeStringIfNotEqual(buf, corrected); + if (status != CELIX_SUCCESS) { + celix_err_push("Failed to parse version string"); + return NULL; + } + return version; +} + +static bool celix_properties_isVersionString(const char* value) { + return strncmp(value, "celix_version<", 14) == 0 && value[strlen(value) - 1] == '>'; +} + +/** + * @brief Determine the array list element type based on the json value. + * + * If the array is empty or of a mixed type, the element type cannot be determined and a CELIX_ILLEGAL_ARGUMENT is + * returned. + * + * @param[in] value The json value. + * @param[out] out The array list element type. + * @return CELIX_SUCCESS if the array list element type could be determined or CELIX_ILLEGAL_ARGUMENT if the array + * type could not be determined. + */ +static celix_status_t celix_properties_determineArrayType(const json_t* jsonArray, + celix_array_list_element_type_t* out) { + size_t size = json_array_size(jsonArray); + if (size == 0) { + celix_err_push("Empty array"); + return CELIX_ILLEGAL_ARGUMENT; + } + + json_t* value; + int index; + json_type type = JSON_NULL; + bool versionType = false; + json_array_foreach(jsonArray, index, value) { + if (index == 0) { + type = json_typeof(value); + if (type == JSON_STRING && celix_properties_isVersionString(json_string_value(value))) { + versionType = true; + } + } else if ((type == JSON_TRUE || type == JSON_FALSE) && + (json_typeof(value) == JSON_TRUE || json_typeof(value) == JSON_FALSE)) { + // bool, ok. + continue; + } else if (type != json_typeof(value)) { + celix_err_push("Mixed types in array"); + return CELIX_ILLEGAL_ARGUMENT; + } else if (versionType) { + if (json_typeof(value) != JSON_STRING || !celix_properties_isVersionString(json_string_value(value))) { + celix_err_push("Mixed version and non-version strings in array"); + return CELIX_ILLEGAL_ARGUMENT; + } + } + } + + switch (type) { + case JSON_STRING: + *out = versionType ? CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION : CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING; + break; + case JSON_INTEGER: + *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG; + break; + case JSON_REAL: + *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE; + break; + case JSON_TRUE: + case JSON_FALSE: + *out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL; + break; + default: + celix_err_pushf("Unexpected json array type %d", type); + return CELIX_ILLEGAL_ARGUMENT; + } + + return CELIX_SUCCESS; +} + +static celix_status_t celix_properties_loadArray(celix_properties_t* props, const char* key, const json_t* jsonArray) { + celix_array_list_element_type_t elType; + celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType); + if (status != CELIX_SUCCESS) { + return status; + } + + celix_array_list_create_options_t opts = CELIX_EMPTY_ARRAY_LIST_CREATE_OPTIONS; + opts.elementType = elType; + celix_autoptr(celix_array_list_t) array = celix_arrayList_createWithOptions(&opts); + if (!array) { + return CELIX_ENOMEM; + } + + json_t* value; + int index; + json_array_foreach(jsonArray, index, value) { + switch (elType) { + case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING: + status = celix_arrayList_addString(array, json_string_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG: + status = celix_arrayList_addLong(array, (long)json_integer_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE: + status = celix_arrayList_addDouble(array, json_real_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL: + status = celix_arrayList_addBool(array, json_boolean_value(value)); + break; + case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION: { + celix_version_t* v = celix_properties_parseVersion(json_string_value(value)); + if (!v) { + return CELIX_ILLEGAL_ARGUMENT; + } + status = celix_arrayList_addVersion(array, v); + break; + } + default: + // LCOV_EXCL_START + celix_err_pushf("Unexpected array list element type %d", elType); + return CELIX_ILLEGAL_ARGUMENT; + // LCOV_EXCL_STOP + } + if (status != CELIX_SUCCESS) { + return status; + } + } + return celix_properties_assignArrayList(props, key, celix_steal_ptr(array)); +} + +static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, const json_t* jsonValue) { celix_status_t status = CELIX_SUCCESS; - (void)stream; + if (json_is_string(jsonValue) && celix_properties_isVersionString(json_string_value(jsonValue))) { + celix_version_t* version = celix_properties_parseVersion(json_string_value(jsonValue)); + if (!version) { + return CELIX_ILLEGAL_ARGUMENT; + } + status = celix_properties_setVersion(props, key, version); + } else if (json_is_string(jsonValue)) { + status = celix_properties_setString(props, key, json_string_value(jsonValue)); + } else if (json_is_integer(jsonValue)) { + status = celix_properties_setLong(props, key, json_integer_value(jsonValue)); + } else if (json_is_real(jsonValue)) { + status = celix_properties_setDouble(props, key, json_real_value(jsonValue)); + } else if (json_is_boolean(jsonValue)) { + status = celix_properties_setBool(props, key, json_boolean_value(jsonValue)); + } else if (json_is_object(jsonValue)) { + // TODO + status = CELIX_ILLEGAL_ARGUMENT; + } else if (json_is_array(jsonValue)) { + status = celix_properties_loadArray(props, key, jsonValue); + } else { + // LCOV_EXCL_START + celix_err_pushf("Unexpected json value type"); + return CELIX_ILLEGAL_ARGUMENT; + // LCOV_EXCL_STOP + } + return status; +} + +static celix_status_t celix_properties_loadFromJson(json_t* obj, celix_properties_t** out) { + assert(obj != NULL && json_is_object(obj)); celix_autoptr(celix_properties_t) props = celix_properties_create(); + if (!props) { + return CELIX_ENOMEM; + } + + // add loop (obj=root, prefix="" and extend prefix when going into sub objects) + const char* key; + json_t* value; + json_object_foreach(obj, key, value) { + if (json_is_object(value)) { + // TODO + return CELIX_ILLEGAL_ARGUMENT; + } + celix_status_t status = celix_properties_loadValue(props, key, value); + if (status != CELIX_SUCCESS) { + return status; + } + } + *out = celix_steal_ptr(props); - return status; + return CELIX_SUCCESS; +} + +celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) { + json_error_t jsonError; + json_t* root = json_loadf(stream, 0, &jsonError); + if (!root) { + celix_err_pushf("Failed to parse json: %s", jsonError.text); + return CELIX_ILLEGAL_ARGUMENT; + } + + return celix_properties_loadFromJson(root, out); } \ No newline at end of file
