diff --git a/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc b/libs/utils/gtest/src/PropertiesSerializationTestSuite.cc index a5d6a1df7..4aa2469d5 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 5903ecaa3..e501071c0 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 0b4c5f8b5..a75c9f57c 100644 --- a/libs/utils/src/properties_serialization.c +++ b/libs/utils/src/properties_serialization.c @@ -28,7 +28,7 @@ #include #include -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