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


Reply via email to