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 04bccbd3741e427b52790d4bfc3659f9e30c1583
Author: Pepijn Noltes <pnol...@apache.org>
AuthorDate: Tue Apr 9 20:03:50 2024 +0200

    gh-685: Add support for nested obj properties decoding
---
 .../gtest/src/PropertiesSerializationTestSuite.cc  | 246 ++++++++++++++++-----
 libs/utils/include/celix_properties.h              |  21 +-
 libs/utils/src/properties_serialization.c          |  72 ++++--
 3 files changed, 262 insertions(+), 77 deletions(-)

diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc 
b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
index a5d6a1df..4aa2469d 100644
--- a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
@@ -32,7 +32,7 @@ class PropertiesSerializationTestSuite : public 
::testing::Test {
     PropertiesSerializationTestSuite() { celix_err_resetErrors(); }
 };
 
-TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) {
+TEST_F(PropertiesSerializationTestSuite, EncodeEmptyPropertiesTest) {
     //Given an empty properties object
     celix_autoptr(celix_properties_t) props = celix_properties_create();
 
@@ -41,16 +41,16 @@ TEST_F(PropertiesSerializationTestSuite, 
SaveEmptyPropertiesTest) {
     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);
+    //When encoding the properties to the stream
+    auto status = celix_properties_encodeToStream(props, stream, 0);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the stream contains an empty JSON object
     fclose(stream);
     EXPECT_STREQ("{}", buf);
 }
 
-TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingleValuesTest) {
+TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSingleValuesTest) 
{
         //Given a properties object with single values
         celix_autoptr(celix_properties_t) props = celix_properties_create();
         celix_properties_set(props, "key1", "value1");
@@ -65,9 +65,9 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithSingleValuesTest) {
         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);
+        //When encoding the properties to the stream
+        auto status = celix_properties_encodeToStream(props, stream, 0);
+        ASSERT_EQ(CELIX_SUCCESS, status);
 
         //Then the stream contains the JSON representation snippets of the 
properties
         fclose(stream);
@@ -76,8 +76,6 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithSingleValuesTest) {
         EXPECT_NE(nullptr, strstr(buf, R"("key3":3)")) << "JSON: " << buf;
         EXPECT_NE(nullptr, strstr(buf, R"("key4":4.0)")) << "JSON: " << buf;
         EXPECT_NE(nullptr, strstr(buf, R"("key5":true)")) << "JSON: " << buf;
-
-        //TODO how are versions serialized? A string representation is needed 
to reconstruct the version from JSON
         EXPECT_NE(nullptr, strstr(buf, 
R"("key6":"celix_version<1.2.3.qualifier>")")) << "JSON: " << buf;
 
         //And the buf is a valid JSON object
@@ -87,7 +85,7 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithSingleValuesTest) {
         json_decref(root);
 }
 
-TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithNaNAndInfValuesTest) {
+TEST_F(PropertiesSerializationTestSuite, 
EncodePropertiesWithNaNAndInfValuesTest) {
     //Given a NAN, INF and -INF value
     auto keys = {"NAN", "INF", "-INF"};
     for (const auto& key : keys) {
@@ -104,7 +102,7 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithNaNAndInfValuesTest)
 
         //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);
+        auto status = celix_properties_encodeToStream(props, stream, 0);
         EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
 
         //And an error msg is added to celix_err
@@ -113,7 +111,7 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithNaNAndInfValuesTest)
 }
 
 
-TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithArrayListsTest) {
+TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithArrayListsTest) {
     // 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();
@@ -143,8 +141,8 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithArrayListsTest) {
     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);
+    auto status = celix_properties_encodeToStream(props, stream, 0);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     // Then the stream contains the JSON representation snippets of the 
properties
     fclose(stream);
@@ -163,7 +161,7 @@ TEST_F(PropertiesSerializationTestSuite, 
SavePropertiesWithArrayListsTest) {
 }
 
 
-TEST_F(PropertiesSerializationTestSuite, SaveEmptyArrayTest) {
+TEST_F(PropertiesSerializationTestSuite, EncodeEmptyArrayTest) {
     //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());
@@ -178,16 +176,16 @@ TEST_F(PropertiesSerializationTestSuite, 
SaveEmptyArrayTest) {
     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);
+    //When encoding the properties to the stream
+    auto status = celix_properties_encodeToStream(props, stream, 0);
+    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);
 }
 
-TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) {
+TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysTest) {
     //Given a properties object with jpath keys
     celix_autoptr(celix_properties_t) props = celix_properties_create();
     celix_properties_set(props, "key1", "value1");
@@ -202,9 +200,9 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) 
{
     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);
+    //When encoding the properties to the stream
+    auto status = celix_properties_encodeToStream(props, stream, 0);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the stream contains the JSON representation snippets of the 
properties
     fclose(stream);
@@ -221,7 +219,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysTest) 
{
     json_decref(root);
 }
 
-TEST_F(PropertiesSerializationTestSuite, SaveJPathKeysWithCollisionTest) {
+TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) {
     //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");
@@ -234,9 +232,9 @@ TEST_F(PropertiesSerializationTestSuite, 
SaveJPathKeysWithCollisionTest) {
     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);
+    //When encoding the properties to the stream
+    auto status = celix_properties_encodeToStream(props, stream, 0);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the stream contains the JSON representation snippets of the 
properties
     fclose(stream);
@@ -254,15 +252,15 @@ TEST_F(PropertiesSerializationTestSuite, 
SaveJPathKeysWithCollisionTest) {
     json_decref(root);
 }
 
-TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) {
+TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) {
     //Given an empty JSON object
     const char* json = "{}";
     FILE* stream = fmemopen((void*)json, strlen(json), "r");
 
-    //When loading the properties from the stream
+    //When decoding the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_loadFromStream(stream, &props);
-    EXPECT_EQ(CELIX_SUCCESS, status);
+    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the properties object is empty
     EXPECT_EQ(0, celix_properties_size(props));
@@ -270,7 +268,7 @@ TEST_F(PropertiesSerializationTestSuite, 
LoadEmptyPropertiesTest) {
     fclose(stream);
 }
 
-TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithSingleValuesTest) {
+TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSingleValuesTest) 
{
     //Given a JSON object with single values for types string, long, double, 
bool and version
     const char* jsonInput = R"({
         "strKey":"strValue",
@@ -283,11 +281,10 @@ TEST_F(PropertiesSerializationTestSuite, 
LoadPropertiesWithSingleValuesTest) {
     //And a stream with the JSON object
     FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
 
-
-    //When loading the properties from the stream
+    //When decoding the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_loadFromStream(stream, &props);
-    EXPECT_EQ(CELIX_SUCCESS, status);
+    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the properties object contains the single values
     EXPECT_EQ(5, celix_properties_size(props));
@@ -301,26 +298,28 @@ TEST_F(PropertiesSerializationTestSuite, 
LoadPropertiesWithSingleValuesTest) {
     EXPECT_STREQ("1.2.3.qualifier", vStr);
 }
 
-TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithArrayListsTest) {
+TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithArrayListsTest) {
     //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>"]
+        
"versionArr":["celix_version<1.2.3.qualifier>","celix_version<4.5.6.qualifier>"],
+        "mixedRealAndIntArr1":[1,2.0,2,3.0],
+        "mixedRealAndIntArr2":[1.0,2,2.0,3]
     })";
 
     //And a stream with the JSON object
     FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
 
-    //When loading the properties from the stream
+    //When decoding the properties from the stream
     celix_autoptr(celix_properties_t) props = nullptr;
-    auto status = celix_properties_loadFromStream(stream, &props);
-    EXPECT_EQ(CELIX_SUCCESS, status);
+    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
 
     //Then the properties object contains the array values
-    EXPECT_EQ(5, celix_properties_size(props));
+    EXPECT_EQ(7, celix_properties_size(props));
 
     //And the string array is correctly loaded
     auto* strArr = celix_properties_getArrayList(props, "strArr");
@@ -367,24 +366,42 @@ TEST_F(PropertiesSerializationTestSuite, 
LoadPropertiesWithArrayListsTest) {
     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
+    //And the mixed json real and int arrays are correctly loaded as double 
arrays
+    auto* mixedRealAndIntArr1 = celix_properties_getArrayList(props, 
"mixedRealAndIntArr1");
+    ASSERT_NE(nullptr, mixedRealAndIntArr1);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, 
celix_arrayList_getElementType(mixedRealAndIntArr1));
+    EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr1));
+    EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 0));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 1));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 2));
+    EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr1, 3));
+
+    auto* mixedRealAndIntArr2 = celix_properties_getArrayList(props, 
"mixedRealAndIntArr2");
+    ASSERT_NE(nullptr, mixedRealAndIntArr2);
+    EXPECT_EQ(CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE, 
celix_arrayList_getElementType(mixedRealAndIntArr2));
+    EXPECT_EQ(4, celix_arrayList_size(mixedRealAndIntArr2));
+    EXPECT_DOUBLE_EQ(1.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 0));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 1));
+    EXPECT_DOUBLE_EQ(2.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 2));
+    EXPECT_DOUBLE_EQ(3.0, celix_arrayList_getDouble(mixedRealAndIntArr2, 3));
+}
 
-TEST_F(PropertiesSerializationTestSuite, LoadPropertiesWithInvalidInputTest) {
+TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) 
{
     auto invalidInputs = {
         R"({)",                            // invalid JSON (caught by jansson)
-        R"({"emptyArr":[]})",              // Empty array, not supported
+        R"([])",                           // unsupported JSON (top level 
array not supported)
+        R"(42)",                           // invalid JSON (caught by jansson)
         R"({"mixedArr":["string", true]})", // Mixed array, not supported
-        R"({"mixedArr":[1.9, 2]})", // Mixed array, TODO this should be 
supported
+        R"({"key1":null})",                 // Null value, not 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
+        //When decoding the properties from the stream
         celix_autoptr(celix_properties_t) props = nullptr;
-        auto status = celix_properties_loadFromStream(stream, &props);
+        auto status = celix_properties_decodeFromStream(stream, 0, &props);
 
         //Then loading fails
         EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
@@ -397,7 +414,132 @@ TEST_F(PropertiesSerializationTestSuite, 
LoadPropertiesWithInvalidInputTest) {
     }
 }
 
-//TODO test deserialize null values
-//TODO test serialize with empty array (treated as unset)
-//TODO test with jpath subset keys and json serialization
+TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithEmptyArrayTest) {
+    //Given a JSON object with an empty array
+    auto* emptyArrays = R"({"key1":[]})";
+
+    //And a stream with the JSON object
+    FILE* stream = fmemopen((void*)emptyArrays, strlen(emptyArrays), "r");
+
+    //When decoding the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_decodeFromStream(stream, 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));
+}
+
+TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithNestedObjectsTest) {
+    // Given a complex JSON object
+    const char* jsonInput = R"({
+        "key1":"value1",
+        "key2":"value2",
+        "object1": {
+            "key3":"value3",
+            "key4":true
+        },
+        "object2": {
+            "key5":5.0
+        },
+        "object3":{
+            "object4":{
+                "key6":6
+            }
+        }
+    })";
+
+    // And a stream with the JSON object
+    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+    // When decoding the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_decodeFromStream(stream, 0, &props);
+    ASSERT_EQ(CELIX_SUCCESS, status);
+
+    // Then the properties object contains the nested objects
+    EXPECT_EQ(6, celix_properties_size(props));
+    EXPECT_STREQ("value1", celix_properties_getString(props, "key1"));
+    EXPECT_STREQ("value2", celix_properties_getString(props, "key2"));
+    EXPECT_STREQ("value3", celix_properties_getString(props, "object1/key3"));
+    EXPECT_EQ(true, celix_properties_getBool(props, "object1/key4", false));
+    EXPECT_DOUBLE_EQ(5., celix_properties_getDouble(props, "object2/key5", 
0.0));
+    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
+    const char* jsonInput = R"({
+        "object1": {
+            "object2": {
+                "key1":true
+            }
+        },
+        "object1/object2/key1":6
+    })";
+
+    // And a stream with the JSON object
+    FILE* stream = fmemopen((void*)jsonInput, strlen(jsonInput), "r");
+
+    // When decoding the properties from the stream
+    celix_autoptr(celix_properties_t) props = nullptr;
+    auto status = celix_properties_decodeFromStream(stream, 0, &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");
+}
+
+//TODO
+//TEST_F(PropertiesSerializationTestSuite, 
DecodePropertiesWithStrictEnabledDisabledTest) {
+//    auto invalidInputs = {
+//        R"({"mixedArr":["string", true]})", // Mixed array gives error on 
strict
+//        R"({"key1":null})",                 // Null value gives error on 
strict
+//        R"({"":"value"})",                  // "" key gives error on strict
+//        R"({"emptyArr":[]})",               // Empty array gives error on 
strict
+//        R"({"key1":"val1", "key1:"val2"})", // Duplicate key gives error on 
strict
+//    };
+//
+//    for (auto& invalidInput: invalidInputs) {
+//        //Given an invalid JSON object
+//        FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), 
"r");
+//
+//        //When decoding the properties from the stream with an empty flags
+//        celix_autoptr(celix_properties_t) props = nullptr;
+//        auto status = celix_properties_decodeFromStream(stream, 0, &props);
+//
+//        //Then decoding succeeds, because strict is disabled
+//        ASSERT_EQ(CELIX_SUCCESS, status);
+//        EXPECT_GE(celix_err_getErrorCount(), 0);
+//
+//        //But the properties object is empty, because the invalid input is 
ignored
+//        EXPECT_EQ(0, celix_properties_size(props));
+//
+//        fclose(stream);
+//    }
+//
+//    for (auto& invalidInput: invalidInputs) {
+//        //Given an invalid JSON object
+//        FILE* stream = fmemopen((void*)invalidInput, strlen(invalidInput), 
"r");
+//
+//        //When decoding 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);
+//
+//        //Then decoding 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 with key starting and ending with slash
diff --git a/libs/utils/include/celix_properties.h 
b/libs/utils/include/celix_properties.h
index 5903ecaa..e501071c 100644
--- a/libs/utils/include/celix_properties.h
+++ b/libs/utils/include/celix_properties.h
@@ -180,11 +180,28 @@ CELIX_UTILS_EXPORT celix_status_t 
celix_properties_store(celix_properties_t* pro
                                                          const char* file,
                                                          const char* header);
 
+#define CELIX_PROPERTIES_ENCODE_PRETTY                  0x01
+#define CELIX_PROPERTIES_ENCODE_SORT_KEYS               0x02
+
+#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
-CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const 
celix_properties_t* properties, FILE* stream);
+CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const 
celix_properties_t* properties,
+                                                                  FILE* stream,
+                                                                  int 
encodeFlags);
 
 //TODO doc
-CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* 
stream, celix_properties_t** out);
+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.
diff --git a/libs/utils/src/properties_serialization.c 
b/libs/utils/src/properties_serialization.c
index 0b4c5f8b..a75c9f57 100644
--- a/libs/utils/src/properties_serialization.c
+++ b/libs/utils/src/properties_serialization.c
@@ -28,7 +28,7 @@
 #include <math.h>
 #include <string.h>
 
-static celix_status_t celix_properties_loadValue(celix_properties_t* props, 
const char* key, const json_t* jsonValue);
+static celix_status_t celix_properties_loadValue(celix_properties_t* props, 
const char* key, json_t* jsonValue);
 
 // TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new 
and json_dumpf for error injection
 
@@ -211,7 +211,7 @@ celix_properties_addEntryToJson(const 
celix_properties_entry_t* entry, const cha
     return CELIX_SUCCESS;
 }
 
-celix_status_t celix_properties_saveToStream(const celix_properties_t* 
properties, FILE* stream) {
+celix_status_t celix_properties_encodeToStream(const celix_properties_t* 
properties, FILE* stream, int encodeFlags) {
     json_t* root = json_object();
     if (!root) {
         celix_err_push("Failed to create json object");
@@ -262,7 +262,7 @@ static bool celix_properties_isVersionString(const char* 
value) {
 /**
  * @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
+ * If the array is of a mixed type, the element type cannot be determined and 
a CELIX_ILLEGAL_ARGUMENT is
  * returned.
  *
  * @param[in] value The json value.
@@ -273,10 +273,7 @@ static bool celix_properties_isVersionString(const char* 
value) {
 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;
-    }
+    assert(size > 0); //precondition: size > 0
 
     json_t* value;
     int index;
@@ -288,10 +285,16 @@ static celix_status_t 
celix_properties_determineArrayType(const json_t* jsonArra
             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)) {
+        } else if ((type == JSON_TRUE || type == JSON_FALSE) && 
json_is_boolean(value)) {
             // bool, ok.
             continue;
+        } else if (type == JSON_INTEGER && json_typeof(value) == JSON_REAL) {
+            // mixed integer and real, ok but promote to real
+            type = JSON_REAL;
+            continue;
+        } else if (type == JSON_REAL && json_typeof(value) == JSON_INTEGER) {
+            // mixed real and integer, ok
+            continue;
         } else if (type != json_typeof(value)) {
             celix_err_push("Mixed types in array");
             return CELIX_ILLEGAL_ARGUMENT;
@@ -350,7 +353,7 @@ static celix_status_t 
celix_properties_loadArray(celix_properties_t* props, cons
             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));
+            status = celix_arrayList_addDouble(array, 
json_number_value(value));
             break;
         case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL:
             status = celix_arrayList_addBool(array, json_boolean_value(value));
@@ -376,7 +379,12 @@ static celix_status_t 
celix_properties_loadArray(celix_properties_t* props, cons
     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) {
+static celix_status_t celix_properties_loadValue(celix_properties_t* props, 
const char* key, json_t* jsonValue) {
+    if (celix_properties_hasKey(props, key)) {
+        celix_err_pushf("Key `%s` already exists.", key);
+        return CELIX_ILLEGAL_ARGUMENT;
+    }
+
     celix_status_t status = CELIX_SUCCESS;
     if (json_is_string(jsonValue) && 
celix_properties_isVersionString(json_string_value(jsonValue))) {
         celix_version_t* version = 
celix_properties_parseVersion(json_string_value(jsonValue));
@@ -393,34 +401,52 @@ static celix_status_t 
celix_properties_loadValue(celix_properties_t* props, cons
     } 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;
+        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;
+            }
+            status = celix_properties_loadValue(props, subKey, fieldValue);
+            if (status != CELIX_SUCCESS) {
+                return status;
+            }
+        }
+        return CELIX_SUCCESS;
+    } else if (json_is_array(jsonValue) && json_array_size(jsonValue) == 0) {
+        // empty array -> treat as unset property. silently ignore
+        return CELIX_SUCCESS;
     } else if (json_is_array(jsonValue)) {
         status = celix_properties_loadArray(props, key, jsonValue);
+    } else if (json_is_null(jsonValue)) {
+        celix_err_pushf("Unexpected null value for key `%s`", key);
+        return CELIX_ILLEGAL_ARGUMENT;
     } else {
         // LCOV_EXCL_START
-        celix_err_pushf("Unexpected json value type");
+        celix_err_pushf("Unexpected json value type for key `%s`", key);
         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));
+static celix_status_t celix_properties_decodeFromJson(json_t* obj, 
celix_properties_t** out) {
+    if (!json_is_object(obj)) {
+        celix_err_push("Expected json object");
+        return CELIX_ILLEGAL_ARGUMENT;
+    }
+
     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;
@@ -431,7 +457,7 @@ static celix_status_t celix_properties_loadFromJson(json_t* 
obj, celix_propertie
     return CELIX_SUCCESS;
 }
 
-celix_status_t celix_properties_loadFromStream(FILE* stream, 
celix_properties_t** out) {
+celix_status_t celix_properties_decodeFromStream(FILE* stream, int 
decodeFlags, celix_properties_t** out) {
     json_error_t jsonError;
     json_t* root = json_loadf(stream, 0, &jsonError);
     if (!root) {
@@ -439,5 +465,5 @@ celix_status_t celix_properties_loadFromStream(FILE* 
stream, celix_properties_t*
         return CELIX_ILLEGAL_ARGUMENT;
     }
 
-    return celix_properties_loadFromJson(root, out);
+    return celix_properties_decodeFromJson(root, out);
 }
\ No newline at end of file

Reply via email to