You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@celix.apache.org by pn...@apache.org on 2024/04/09 21:16:17 UTC
(celix) branch feature/685-properties-json-serialization created (now 72c785df)
This is an automated email from the ASF dual-hosted git repository.
pnoltes pushed a change to branch feature/685-properties-json-serialization
in repository https://gitbox.apache.org/repos/asf/celix.git
at 72c785df gh-685: Add initial setup for properties decode flags
This branch includes the following new commits:
new 3acdb746 gh-685: Initial properties save/load setup
new 9511610b gh-685: Add jansson dep for simple properties save/load
new de072564 gh-685: Add json prop loading for primitive and initial arr.
new 04bccbd3 gh-685: Add support for nested obj properties decoding
new 77cd6b37 gh-685: Rename prop serialization sources to prop encoding
new 72c785df gh-685: Add initial setup for properties decode flags
The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(celix) 02/06: gh-685: Add jansson dep for simple properties save/load
Posted by pn...@apache.org.
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 9511610bb2c7379570c1bd3a9705202c0723a865
Author: Pepijn Noltes <pn...@apache.org>
AuthorDate: Tue Apr 2 20:25:09 2024 +0200
gh-685: Add jansson dep for simple properties save/load
---
libs/utils/CMakeLists.txt | 3 +-
.../gtest/src/PropertiesSerializationTestSuite.cc | 70 ++++++++++++-
libs/utils/src/properties.c | 2 +-
libs/utils/src/properties_serialization.c | 112 ++++++++++++++++++++-
4 files changed, 180 insertions(+), 7 deletions(-)
diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt
index 064c9683..4180b48b 100644
--- a/libs/utils/CMakeLists.txt
+++ b/libs/utils/CMakeLists.txt
@@ -18,6 +18,7 @@
celix_subproject(UTILS "Option to enable building the Utilities library" ON)
if (UTILS)
find_package(libzip REQUIRED)
+ find_package(jansson REQUIRED) #TODO add jansson dep info to build (conan) and documentation info
set(MEMSTREAM_SOURCES )
set(MEMSTREAM_INCLUDES )
@@ -42,7 +43,7 @@ if (UTILS)
src/celix_cleanup.c
${MEMSTREAM_SOURCES}
)
- set(UTILS_PRIVATE_DEPS libzip::zip)
+ set(UTILS_PRIVATE_DEPS libzip::zip jansson::jansson)
set(UTILS_PUBLIC_DEPS)
add_library(utils SHARED ${UTILS_SRC})
diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
index d3bf6aa0..99cbeceb 100644
--- a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
@@ -18,9 +18,11 @@
*/
#include <gtest/gtest.h>
+#include <jansson.h>
#include "celix_err.h"
#include "celix_properties.h"
+#include "celix_stdlib_cleanup.h"
using ::testing::MatchesRegex;
@@ -34,7 +36,7 @@ TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) {
celix_autoptr(celix_properties_t) props = celix_properties_create();
//And an in-memory stream
- char* buf = nullptr;
+ celix_autofree char* buf = nullptr;
size_t bufLen = 0;
FILE* stream = open_memstream(&buf, &bufLen);
@@ -62,3 +64,69 @@ TEST_F(PropertiesSerializationTestSuite, LoadEmptyPropertiesTest) {
fclose(stream);
}
+
+TEST_F(PropertiesSerializationTestSuite, SavePropertiesWithSingelValuesTest) {
+ //Given a properties object with single values
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "key1", "value1");
+ celix_properties_set(props, "key2", "value2");
+ celix_properties_setLong(props, "key3", 3);
+ celix_properties_setDouble(props, "key4", 4.0);
+ celix_properties_setBool(props, "key5", true);
+ celix_properties_assignVersion(props, "key6", celix_version_create(1, 2, 3, "qualifier"));
+
+ //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")")) << "JSON: " << buf;
+ EXPECT_NE(nullptr, strstr(buf, R"("key2":"value2")")) << "JSON: " << buf;
+ 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
+ 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, SavePropertiesWithArrayListsTest) {
+ //Given a properties object with array list values
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_array_list_t* list1 = celix_arrayList_createStringArray();
+ celix_arrayList_addString(list1, "value1");
+ celix_arrayList_addString(list1, "value2");
+ celix_properties_assignArrayList(props, "key1", list1);
+ //TODO long, double, bool, version
+
+ //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;
+
+ //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);
+}
diff --git a/libs/utils/src/properties.c b/libs/utils/src/properties.c
index c5100112..024d849d 100644
--- a/libs/utils/src/properties.c
+++ b/libs/utils/src/properties.c
@@ -645,7 +645,7 @@ const celix_properties_entry_t* celix_properties_getEntry(const celix_properties
return entry;
}
-static const bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry,
+static bool celix_properties_isEntryArrayListWithElType(const celix_properties_entry_t* entry,
celix_array_list_element_type_t elType) {
return entry != NULL && entry->valueType == CELIX_PROPERTIES_VALUE_TYPE_ARRAY_LIST &&
celix_arrayList_getElementType(entry->typed.arrayValue) == elType;
diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_serialization.c
index 4ebda01e..d52fa6ef 100644
--- a/libs/utils/src/properties_serialization.c
+++ b/libs/utils/src/properties_serialization.c
@@ -19,11 +19,115 @@
#include "celix_properties.h"
+#include "celix_err.h"
+#include "celix_stdlib_cleanup.h"
+
+#include <jansson.h>
+
+//TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection
+
+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);
+}
+
+static json_t* celix_properties_arrayElementEntryValueToJson(celix_array_list_element_type_t elType,
+ celix_array_list_entry_t entry) {
+ switch (elType) {
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_STRING:
+ return json_string(entry.stringVal);
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_LONG:
+ return json_integer(entry.longVal);
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_DOUBLE:
+ return json_real(entry.doubleVal);
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL:
+ return json_boolean(entry.boolVal);
+ case CELIX_ARRAY_LIST_ELEMENT_TYPE_VERSION:
+ celix_properties_versionToJson(entry.versionVal);
+ default:
+ // LCOV_EXCL_START
+ celix_err_pushf("Unexpected array list element type %d", elType);
+ return NULL;
+ // LCOV_EXCL_STOP
+ }
+}
+
+static json_t* celix_properties_arrayEntryValueToJson(const celix_properties_entry_t* entry) {
+ json_t* array = json_array();
+ if (!array) {
+ celix_err_push("Failed to create json array");
+ return NULL;
+ }
+
+ 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_decref(array);
+ return NULL;
+ }
+ }
+ return array;
+}
+
+static json_t* celix_properties_entryValueToJson(const celix_properties_entry_t* entry) {
+ 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
+ }
+}
+
celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) {
- celix_status_t status = CELIX_SUCCESS;
- (void)properties;
- fprintf(stream, "{}");
- return status;
+ json_t* root = json_object();
+ if (!root) {
+ celix_err_push("Failed to create json object");
+ }
+
+ 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");
+ json_decref(root);
+ return CELIX_ENOMEM; //TODO improve error
+ }
+ }
+
+ 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_SUCCESS;
}
celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) {
(celix) 04/06: gh-685: Add support for nested obj properties decoding
Posted by pn...@apache.org.
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 <pn...@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
(celix) 05/06: gh-685: Rename prop serialization sources to prop encoding
Posted by pn...@apache.org.
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 77cd6b37e7bae39663b20dee7c6fb31593a00c89
Author: Pepijn Noltes <pn...@apache.org>
AuthorDate: Tue Apr 9 20:05:33 2024 +0200
gh-685: Rename prop serialization sources to prop encoding
---
libs/utils/CMakeLists.txt | 2 +-
libs/utils/gtest/CMakeLists.txt | 2 +-
...opertiesSerializationTestSuite.cc => PropertiesEncodingTestSuite.cc} | 0
libs/utils/src/{properties_serialization.c => properties_encoding.c} | 0
4 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt
index 4180b48b..cec80f41 100644
--- a/libs/utils/CMakeLists.txt
+++ b/libs/utils/CMakeLists.txt
@@ -30,7 +30,7 @@ if (UTILS)
src/version.c
src/version_range.c
src/properties.c
- src/properties_serialization.c
+ src/properties_encoding.c
src/utils.c
src/filter.c
src/celix_log_level.c
diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt
index 62c1ac0c..11c6ec00 100644
--- a/libs/utils/gtest/CMakeLists.txt
+++ b/libs/utils/gtest/CMakeLists.txt
@@ -29,7 +29,7 @@ add_executable(test_utils
src/CelixUtilsTestSuite.cc
src/ConvertUtilsTestSuite.cc
src/PropertiesTestSuite.cc
- src/PropertiesSerializationTestSuite.cc
+ src/PropertiesEncodingTestSuite.cc
src/VersionTestSuite.cc
src/ErrTestSuite.cc
src/ThreadsTestSuite.cc
diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
similarity index 100%
rename from libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
rename to libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
diff --git a/libs/utils/src/properties_serialization.c b/libs/utils/src/properties_encoding.c
similarity index 100%
rename from libs/utils/src/properties_serialization.c
rename to libs/utils/src/properties_encoding.c
(celix) 03/06: gh-685: Add json prop loading for primitive and initial arr.
Posted by pn...@apache.org.
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 <pn...@apache.org>
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
(celix) 01/06: gh-685: Initial properties save/load setup
Posted by pn...@apache.org.
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 3acdb746e55738f45134a6412602f66fc9b80a39
Author: Pepijn Noltes <pn...@apache.org>
AuthorDate: Mon Apr 1 20:28:02 2024 +0200
gh-685: Initial properties save/load setup
---
libs/utils/CMakeLists.txt | 1 +
libs/utils/gtest/CMakeLists.txt | 1 +
.../gtest/src/PropertiesSerializationTestSuite.cc | 64 ++++++++++++++++++++++
libs/utils/include/celix_properties.h | 6 ++
libs/utils/src/properties_serialization.c | 35 ++++++++++++
5 files changed, 107 insertions(+)
diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt
index e1b49835..064c9683 100644
--- a/libs/utils/CMakeLists.txt
+++ b/libs/utils/CMakeLists.txt
@@ -29,6 +29,7 @@ if (UTILS)
src/version.c
src/version_range.c
src/properties.c
+ src/properties_serialization.c
src/utils.c
src/filter.c
src/celix_log_level.c
diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt
index d04f9e30..62c1ac0c 100644
--- a/libs/utils/gtest/CMakeLists.txt
+++ b/libs/utils/gtest/CMakeLists.txt
@@ -29,6 +29,7 @@ add_executable(test_utils
src/CelixUtilsTestSuite.cc
src/ConvertUtilsTestSuite.cc
src/PropertiesTestSuite.cc
+ src/PropertiesSerializationTestSuite.cc
src/VersionTestSuite.cc
src/ErrTestSuite.cc
src/ThreadsTestSuite.cc
diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
new file mode 100644
index 00000000..d3bf6aa0
--- /dev/null
+++ b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "celix_err.h"
+#include "celix_properties.h"
+
+using ::testing::MatchesRegex;
+
+class PropertiesSerializationTestSuite : public ::testing::Test {
+ public:
+ PropertiesSerializationTestSuite() { celix_err_resetErrors(); }
+};
+
+TEST_F(PropertiesSerializationTestSuite, SaveEmptyPropertiesTest) {
+ //Given an empty properties object
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+
+ //And an in-memory stream
+ 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
+ fclose(stream);
+ 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);
+}
diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h
index 39e87c66..5903ecaa 100644
--- a/libs/utils/include/celix_properties.h
+++ b/libs/utils/include/celix_properties.h
@@ -180,6 +180,12 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro
const char* file,
const char* header);
+//TODO doc
+CELIX_UTILS_EXPORT celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream);
+
+//TODO doc
+CELIX_UTILS_EXPORT celix_status_t celix_properties_loadFromStream(FILE* stream, 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
new file mode 100644
index 00000000..4ebda01e
--- /dev/null
+++ b/libs/utils/src/properties_serialization.c
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "celix_properties.h"
+
+celix_status_t celix_properties_saveToStream(const celix_properties_t* properties, FILE* stream) {
+ celix_status_t status = CELIX_SUCCESS;
+ (void)properties;
+ fprintf(stream, "{}");
+ return status;
+}
+
+celix_status_t celix_properties_loadFromStream(FILE* stream, celix_properties_t** out) {
+ celix_status_t status = CELIX_SUCCESS;
+ (void)stream;
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ *out = celix_steal_ptr(props);
+ return status;
+}
\ No newline at end of file
(celix) 06/06: gh-685: Add initial setup for properties decode flags
Posted by pn...@apache.org.
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 72c785dfc706630c87322604b4363542ed9c0f5f
Author: Pepijn Noltes <pn...@apache.org>
AuthorDate: Tue Apr 9 23:16:02 2024 +0200
gh-685: Add initial setup for properties decode flags
---
.../utils/gtest/src/PropertiesEncodingTestSuite.cc | 197 +++++++++++++++------
libs/utils/gtest/src/PropertiesTestSuite.cc | 7 +
libs/utils/include/celix_properties.h | 14 +-
libs/utils/src/properties_encoding.c | 81 ++++++---
4 files changed, 217 insertions(+), 82 deletions(-)
diff --git a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
index 4aa2469d..d346f5f6 100644
--- a/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesEncodingTestSuite.cc
@@ -252,6 +252,49 @@ TEST_F(PropertiesSerializationTestSuite, EncodeJPathKeysWithCollisionTest) {
json_decref(root);
}
+
+//TODO check desired behaviour, currently every "/" leads to a new object (except if an collision occurs)
+//TEST_F(PropertiesSerializationTestSuite, EncodePropertiesWithSpecialKeyNamesTest) {
+// //Given a properties set with special key names (slashes)
+// celix_autoptr(celix_properties_t) props = celix_properties_create();
+// celix_properties_set(props, "/", "value1");
+// celix_properties_set(props, "keyThatEndsWithSlash/", "value2");
+// celix_properties_set(props, "key//With//Double//Slash", "value3");
+// celix_properties_set(props, "object/", "value5");
+// celix_properties_set(props, "object//", "value4");
+// celix_properties_set(props, "object/keyThatEndsWithSlash/", "value6");
+// celix_properties_set(props, "object/key//With//Double//Slash", "value7");
+//
+// //And an in-memory stream
+// celix_autofree char* buf = nullptr;
+// size_t bufLen = 0;
+// FILE* stream = open_memstream(&buf, &bufLen);
+//
+// //When encoding the properties to the stream
+// auto status = celix_properties_encodeToStream(props, stream, 0);
+// ASSERT_EQ(CELIX_SUCCESS, status);
+//
+// std::cout << buf << std::endl;
+//
+// //Then the stream contains the JSON representation snippets of the properties
+// fclose(stream);
+// EXPECT_NE(nullptr, strstr(buf, R"("/":"value1")")) << "JSON: " << buf;
+// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value2")")) << "JSON: " << buf;
+// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value3")")) << "JSON: " << buf;
+// EXPECT_NE(nullptr, strstr(buf, R"("object/":"value5")")) << "JSON: " << buf;
+// EXPECT_NE(nullptr, strstr(buf, R"("/":"value5")")) << "JSON: " << buf; //child of object
+// EXPECT_NE(nullptr, strstr(buf, R"("keyThatEndsWithSlash/":"value6")")) << "JSON: " << buf; //child of object
+// EXPECT_NE(nullptr, strstr(buf, R"("key//With//Double//Slash":"value7")")) << "JSON: " << buf; //child of object
+//
+//
+// //And the buf is a valid JSON object
+// json_error_t error;
+// json_t* root = json_loads(buf, 0, &error);
+// EXPECT_NE(nullptr, root) << "Unexpected JSON error: " << error.text;
+// json_decref(root);
+//}
+
+
TEST_F(PropertiesSerializationTestSuite, DecodeEmptyPropertiesTest) {
//Given an empty JSON object
const char* json = "{}";
@@ -392,8 +435,6 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithInvalidInputTest) {
R"({)", // invalid JSON (caught by jansson)
R"([])", // unsupported JSON (top level array not supported)
R"(42)", // invalid JSON (caught by jansson)
- R"({"mixedArr":["string", true]})", // Mixed array, not supported
- R"({"key1":null})", // Null value, not supported
};
for (auto& invalidInput: invalidInputs) {
//Given an invalid JSON object
@@ -477,7 +518,9 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa
"key1":true
}
},
- "object1/object2/key1":6
+ "object1/object2/key1":6,
+ "key2":2,
+ "key2":3
})";
// And a stream with the JSON object
@@ -487,7 +530,22 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa
celix_autoptr(celix_properties_t) props = nullptr;
auto status = celix_properties_decodeFromStream(stream, 0, &props);
- // Then loading fails
+ // Then loading succeeds
+ ASSERT_EQ(CELIX_SUCCESS, status);
+
+ // And the properties object contains the last values of the jpath keys
+ EXPECT_EQ(2, celix_properties_size(props));
+ EXPECT_EQ(6, celix_properties_getLong(props, "object1/object2/key1", 0));
+ EXPECT_EQ(3, celix_properties_getLong(props, "key2", 0));
+
+ // When the stream is reset
+ fseek(stream, 0, SEEK_SET);
+
+ // And decoding the properties from the stream using a flog that does not allow collisions
+ celix_autoptr(celix_properties_t) props2 = nullptr;
+ status = celix_properties_decodeFromStream(stream, CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES, &props2);
+
+ // Then loading fails, because of a duplicate key
EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, status);
// And at least one error message is added to celix_err
@@ -495,51 +553,88 @@ TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithNestedObjectsAndJPa
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);
-// }
-//}
+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
+ R"({"nullArr":[null,null]})", // Array with null values 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);
+ celix_err_printErrors(stderr, "Error: ", "\n");
+
+ //Then decoding succeeds, because strict is disabled
+ ASSERT_EQ(CELIX_SUCCESS, status);
+ EXPECT_GE(celix_err_getErrorCount(), 0);
+
+ //But the properties size is 0 or 1, because the all invalid inputs are ignored, except the duplicate key
+ auto size = celix_properties_size(props);
+ EXPECT_TRUE(size == 0 || size == 1);
+
+ 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);
+ }
+}
+
+TEST_F(PropertiesSerializationTestSuite, DecodePropertiesWithSpecialKeyNamesTest) {
+ // Given a complex JSON object
+ const char* jsonInput = R"({
+ "/": "value1",
+ "keyThatEndsWithSlash/": "value2",
+ "key//With//Double//Slash": "value3",
+ "object": {
+ "/": "value4",
+ "keyThatEndsWithSlash/": "value5",
+ "key//With//Double//Slash": "value6"
+ }
+ })";
+
+ // 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);
+ celix_err_printErrors(stderr, "Error: ", "\n");
+ 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, "/"));
+ EXPECT_STREQ("value2", celix_properties_getString(props, "keyThatEndsWithSlash/"));
+ EXPECT_STREQ("value3", celix_properties_getString(props, "key//With//Double//Slash"));
+ EXPECT_STREQ("value4", celix_properties_getString(props, "object//"));
+ EXPECT_STREQ("value5", celix_properties_getString(props, "object/keyThatEndsWithSlash/"));
+ EXPECT_STREQ("value6", celix_properties_getString(props, "object/key//With//Double//Slash"));
+}
-//TODO test with key starting and ending with slash
+//TODO test with invalid version string
+//TODO is there a strict option needed for version (e.g. not parseable as version handle as string)
+//TODO test encoding flags
+//TODO error injection tests and wrappers for jansson functions
\ No newline at end of file
diff --git a/libs/utils/gtest/src/PropertiesTestSuite.cc b/libs/utils/gtest/src/PropertiesTestSuite.cc
index 9c1825b5..e1b9391b 100644
--- a/libs/utils/gtest/src/PropertiesTestSuite.cc
+++ b/libs/utils/gtest/src/PropertiesTestSuite.cc
@@ -959,3 +959,10 @@ TEST_F(PropertiesTestSuite, SetArrayListWithIllegalArgumentsTest) {
//And when an NULL key is used, a ILLEGAL_ARGUMENT error is returned
EXPECT_EQ(CELIX_ILLEGAL_ARGUMENT, celix_properties_setArrayList(props, nullptr, list));
}
+
+TEST_F(PropertiesTestSuite, EmptyStringKeyTest) {
+ celix_autoptr(celix_properties_t) props = celix_properties_create();
+ celix_properties_set(props, "", "value"); // "" is a valid key (nullptr is not)
+ EXPECT_EQ(1, celix_properties_size(props));
+ EXPECT_STREQ("value", celix_properties_getString(props, ""));
+}
diff --git a/libs/utils/include/celix_properties.h b/libs/utils/include/celix_properties.h
index e501071c..b82ad9ad 100644
--- a/libs/utils/include/celix_properties.h
+++ b/libs/utils/include/celix_properties.h
@@ -180,9 +180,16 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro
const char* file,
const char* header);
+//TODO document the encode flags
#define CELIX_PROPERTIES_ENCODE_PRETTY 0x01
#define CELIX_PROPERTIES_ENCODE_SORT_KEYS 0x02
+//TODO doc. Not encode does not reset the stream position.
+CELIX_UTILS_EXPORT celix_status_t celix_properties_encodeToStream(const celix_properties_t* properties,
+ FILE* stream,
+ int encodeFlags);
+
+//TODO document the decode flags
#define CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES 0x01
#define CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES 0x02
#define CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS 0x04
@@ -193,12 +200,7 @@ CELIX_UTILS_EXPORT celix_status_t celix_properties_store(celix_properties_t* pro
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_encodeToStream(const celix_properties_t* properties,
- FILE* stream,
- int encodeFlags);
-
-//TODO doc
+//TODO doc. Note decode does not reset the stream position.
CELIX_UTILS_EXPORT celix_status_t celix_properties_decodeFromStream(FILE* stream,
int decodeFlags,
celix_properties_t** out);
diff --git a/libs/utils/src/properties_encoding.c b/libs/utils/src/properties_encoding.c
index a75c9f57..ba30b670 100644
--- a/libs/utils/src/properties_encoding.c
+++ b/libs/utils/src/properties_encoding.c
@@ -28,7 +28,8 @@
#include <math.h>
#include <string.h>
-static celix_status_t celix_properties_loadValue(celix_properties_t* props, const char* key, json_t* jsonValue);
+static celix_status_t
+celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags);
// TODO make jansson wrapper header for auto cleanup, wrap json_object_set_new and json_dumpf for error injection
@@ -225,14 +226,19 @@ celix_status_t celix_properties_encodeToStream(const celix_properties_t* propert
}
}
- int rc =
- json_dumpf(root,
- stream,
- JSON_COMPACT); // TODO make celix properties flags for COMPACT and INDENT and maybe other json flags
+ size_t jsonFlags = JSON_COMPACT;
+ if (encodeFlags & CELIX_PROPERTIES_ENCODE_PRETTY) {
+ jsonFlags = JSON_INDENT(2);
+ }
+ if (encodeFlags & CELIX_PROPERTIES_ENCODE_SORT_KEYS) {
+ jsonFlags |= JSON_SORT_KEYS;
+ }
+
+ int rc = json_dumpf(root, stream, jsonFlags);
json_decref(root);
if (rc != 0) {
celix_err_push("Failed to dump json object");
- return CELIX_ENOMEM; // TODO improve error
+ return CELIX_ENOMEM;
}
return CELIX_SUCCESS;
}
@@ -296,11 +302,9 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra
// mixed real and integer, 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;
}
}
@@ -320,19 +324,25 @@ static celix_status_t celix_properties_determineArrayType(const json_t* jsonArra
case JSON_FALSE:
*out = CELIX_ARRAY_LIST_ELEMENT_TYPE_BOOL;
break;
+ case JSON_NULL:
default:
- celix_err_pushf("Unexpected json array type %d", type);
- return CELIX_ILLEGAL_ARGUMENT;
+ return CELIX_ILLEGAL_ARGUMENT; // TODO Add test for this case and maybe return a different error code and log error
}
return CELIX_SUCCESS;
}
-static celix_status_t celix_properties_loadArray(celix_properties_t* props, const char* key, const json_t* jsonArray) {
+static celix_status_t
+celix_properties_decodeArray(celix_properties_t* props, const char* key, const json_t* jsonArray, int flags) {
celix_array_list_element_type_t elType;
celix_status_t status = celix_properties_determineArrayType(jsonArray, &elType);
- if (status != CELIX_SUCCESS) {
+ if (status != CELIX_SUCCESS && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_MIXED_ARRAYS)) {
+ celix_autofree char* arrStr = json_dumps(jsonArray, JSON_ENCODE_ANY);
+ celix_err_pushf("Unsupported mixed or null array for key '%s': %s", key, arrStr);
return status;
+ } else if (status != CELIX_SUCCESS) {
+ //ignore mixed types
+ return CELIX_SUCCESS;
}
celix_array_list_create_options_t opts = CELIX_EMPTY_ARRAY_LIST_CREATE_OPTIONS;
@@ -368,7 +378,7 @@ static celix_status_t celix_properties_loadArray(celix_properties_t* props, cons
}
default:
// LCOV_EXCL_START
- celix_err_pushf("Unexpected array list element type %d", elType);
+ celix_err_pushf("Unexpected array list element type %d for key %s", elType, key);
return CELIX_ILLEGAL_ARGUMENT;
// LCOV_EXCL_STOP
}
@@ -379,8 +389,17 @@ 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, json_t* jsonValue) {
- if (celix_properties_hasKey(props, key)) {
+static celix_status_t
+celix_properties_decodeValue(celix_properties_t* props, const char* key, json_t* jsonValue, int flags) {
+ if (strncmp(key, "", 1) == 0) {
+ if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_KEYS) {
+ celix_err_push("Key cannot be empty");
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ return CELIX_SUCCESS; // ignore empty keys.
+ }
+
+ if (celix_properties_hasKey(props, key) && (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES)) {
celix_err_pushf("Key `%s` already exists.", key);
return CELIX_ILLEGAL_ARGUMENT;
}
@@ -410,20 +429,28 @@ static celix_status_t celix_properties_loadValue(celix_properties_t* props, cons
celix_err_push("Failed to create sub key");
return CELIX_ENOMEM;
}
- status = celix_properties_loadValue(props, subKey, fieldValue);
+ status = celix_properties_decodeValue(props, subKey, fieldValue, flags);
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
+ if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_EMPTY_ARRAYS) {
+ celix_err_pushf("Unexpected empty array for key `%s`", key);
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ // ignore empty arrays
return CELIX_SUCCESS;
} else if (json_is_array(jsonValue)) {
- status = celix_properties_loadArray(props, key, jsonValue);
+ status = celix_properties_decodeArray(props, key, jsonValue, flags);
} else if (json_is_null(jsonValue)) {
- celix_err_pushf("Unexpected null value for key `%s`", key);
- return CELIX_ILLEGAL_ARGUMENT;
+ if (flags & CELIX_PROPERTIES_DECODE_ERROR_ON_NULL_VALUES) {
+ celix_err_pushf("Unexpected null value for key `%s`", key);
+ return CELIX_ILLEGAL_ARGUMENT;
+ }
+ // ignore null values
+ return CELIX_SUCCESS;
} else {
// LCOV_EXCL_START
celix_err_pushf("Unexpected json value type for key `%s`", key);
@@ -433,7 +460,7 @@ static celix_status_t celix_properties_loadValue(celix_properties_t* props, cons
return status;
}
-static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_properties_t** out) {
+static celix_status_t celix_properties_decodeFromJson(json_t* obj, int flags, celix_properties_t** out) {
if (!json_is_object(obj)) {
celix_err_push("Expected json object");
return CELIX_ILLEGAL_ARGUMENT;
@@ -447,7 +474,7 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_propert
const char* key;
json_t* value;
json_object_foreach(obj, key, value) {
- celix_status_t status = celix_properties_loadValue(props, key, value);
+ celix_status_t status = celix_properties_decodeValue(props, key, value, flags);
if (status != CELIX_SUCCESS) {
return status;
}
@@ -459,11 +486,15 @@ static celix_status_t celix_properties_decodeFromJson(json_t* obj, celix_propert
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);
+ size_t jsonFlags = 0;
+ if (decodeFlags & CELIX_PROPERTIES_DECODE_ERROR_ON_DUPLICATES) {
+ jsonFlags = JSON_REJECT_DUPLICATES;
+ }
+ json_t* root = json_loadf(stream, jsonFlags, &jsonError);
if (!root) {
celix_err_pushf("Failed to parse json: %s", jsonError.text);
return CELIX_ILLEGAL_ARGUMENT;
}
- return celix_properties_decodeFromJson(root, out);
-}
\ No newline at end of file
+ return celix_properties_decodeFromJson(root, decodeFlags, out);
+}