feat: introduce support for struct-as-dict serialization (#459)

This extends the functionality of SDBUSCPP_REGISTER_STRUCT macro.

It now provides functionality for serializing a user-defined struct as an a{sv} dictionary, and for deserializing an a{sv} dictionary into a user-defined struct. The former one is achieved by decorating the struct with sdbus::as_dictionary(struct), the latter one is an automatic behavior -- when sdbus-c++ is asked to deserialize into a struct but the data in the message is of type a{sv}, then the dict-to-struct deserialization is performed automatically.

There are some aspects of behavior in the serialization/deserialization functionality that can be customized by the client. Newly introduced SDBUSCPP_ENABLE_NESTED_STRUCT2DICT_SERIALIZATION and SDBUSCPP_ENABLE_RELAXED_DICT2STRUCT_DESERIALIZATION macros serve the purpose.
This commit is contained in:
Stanislav Angelovič
2024-11-20 23:25:13 +01:00
committed by GitHub
parent 8a117f8b42
commit 02ca7212d1
9 changed files with 501 additions and 136 deletions
@@ -48,8 +48,22 @@ using ::testing::ElementsAre;
using ::testing::SizeIs;
using ::testing::NotNull;
using namespace std::chrono_literals;
using namespace std::string_literals;
using namespace sdbus::test;
namespace my {
struct Struct
{
int i;
std::string s;
std::vector<double> l;
friend bool operator==(const Struct &lhs, const Struct &rhs) = default;
};
}
SDBUSCPP_REGISTER_STRUCT(my::Struct, i, s, l);
/*-------------------------------------*/
/* -- TEST CASES -- */
/*-------------------------------------*/
@@ -249,6 +263,21 @@ TYPED_TEST(SdbusTestObject, CanReceiveSignalWhileMakingMethodCall)
EXPECT_TRUE(waitUntil(this->m_proxy->m_gotSignalWithMap));
}
TYPED_TEST(SdbusTestObject, CanSendAndReceiveDictionariesAsCustomStructsImplicitly)
{
// This test demonstrates that sdbus-c++ can send a SDBUSCPP_REGISTER_STRUCT-described struct as a dictionary of strings to variants,
// and that sdbus-c++ can automatically deserialize a dictionary of strings to variants into such a struct (instead of a map).
const my::Struct structSent{3545342, "hello"s, {3.14, 2.4568546}};
my::Struct structReceived;
this->m_proxy->getProxy().callMethod("returnDictionary")
.onInterface("org.sdbuscpp.integrationtests")
.withArguments(sdbus::as_dictionary(structSent))
.storeResultsTo(structReceived);
ASSERT_THAT(structReceived, Eq(structSent));
}
TYPED_TEST(SdbusTestObject, CanAccessAssociatedMethodCallMessageInMethodCallHandler)
{
this->m_proxy->doOperation(10); // This will save pointer to method call message on server side
+5
View File
@@ -246,6 +246,11 @@ void TestAdaptor::sendLargeMessage(const std::map<int, std::string>& /*collectio
//printf("Adaptor: got collection with %zu items", collection.size());
}
std::map<std::string, sdbus::Variant> TestAdaptor::returnDictionary(const std::map<std::string, sdbus::Variant>& dict)
{
return dict;
}
std::string TestAdaptor::state()
{
return m_state;
+2
View File
@@ -85,6 +85,7 @@ protected:
void doPrivilegedStuff() override;
void emitTwoSimpleSignals() override;
void sendLargeMessage(const std::map<int, std::string>& collection) override;
std::map<std::string, sdbus::Variant> returnDictionary(const std::map<std::string, sdbus::Variant>& dict) override;
uint32_t action() override;
void action(const uint32_t& value) override;
@@ -147,6 +148,7 @@ protected:
void doPrivilegedStuff() override {}
void emitTwoSimpleSignals() override {}
void sendLargeMessage(const std::map<int, std::string>&) override {}
std::map<std::string, sdbus::Variant> returnDictionary(const std::map<std::string, sdbus::Variant>&) override { return {}; }
uint32_t action() override { return {}; }
void action(const uint32_t&) override {}
@@ -58,6 +58,7 @@ protected:
, sdbus::registerMethod("doPrivilegedStuff").implementedAs([this](){ return this->doPrivilegedStuff(); }).markAsPrivileged()
, sdbus::registerMethod("emitTwoSimpleSignals").implementedAs([this](){ return this->emitTwoSimpleSignals(); })
, sdbus::registerMethod("sendLargeMessage").implementedAs([this](const std::map<int, std::string>& collection){ this->sendLargeMessage(collection); })
, sdbus::registerMethod("returnDictionary").implementedAs([this](const std::map<std::string, sdbus::Variant>& dictionary){ return this->returnDictionary(dictionary); })
, sdbus::registerSignal("simpleSignal").markAsDeprecated()
, sdbus::registerSignal("signalWithMap").withParameters<std::map<int32_t, std::string>>("aMap")
, sdbus::registerSignal("signalWithVariant").withParameters<sdbus::Variant>("aVariant")
@@ -108,6 +109,7 @@ private:
virtual void doPrivilegedStuff() = 0;
virtual void emitTwoSimpleSignals() = 0;
virtual void sendLargeMessage(const std::map<int, std::string>& collection) = 0;
virtual std::map<std::string, sdbus::Variant> returnDictionary(const std::map<std::string, sdbus::Variant>& dict) = 0;
private:
virtual uint32_t action() = 0;
+142 -2
View File
@@ -36,6 +36,8 @@ using ::testing::StrEq;
using ::testing::Gt;
using ::testing::DoubleEq;
using ::testing::IsNull;
using ::testing::SizeIs;
using ::testing::ElementsAre;
using namespace std::string_literals;
namespace
@@ -53,6 +55,7 @@ namespace sdbus {
template <typename _ElementType>
sdbus::Message& operator<<(sdbus::Message& msg, const std::list<_ElementType>& items)
{
// TODO: This can also be simplified on the basis of a callback (see dictionary...)
msg.openContainer<_ElementType>();
for (const auto& item : items)
@@ -96,7 +99,8 @@ namespace my {
enum class Enum
{
Value1,
Value2
Value2,
Value3
};
struct Struct
@@ -108,10 +112,36 @@ namespace my {
friend bool operator==(const Struct& lhs, const Struct& rhs) = default;
};
struct RelaxedStruct
{
int i;
std::string s;
std::list<double> l;
Enum e;
friend bool operator==(const RelaxedStruct& lhs, const RelaxedStruct& rhs) = default;
};
struct NestedStruct
{
int i;
std::string s;
Enum e;
Struct x;
friend bool operator==(const NestedStruct& lhs, const NestedStruct& rhs) = default;
};
}
SDBUSCPP_REGISTER_STRUCT(my::Struct, i, s, l, e);
SDBUSCPP_ENABLE_RELAXED_DICT2STRUCT_DESERIALIZATION(my::RelaxedStruct);
SDBUSCPP_REGISTER_STRUCT(my::RelaxedStruct, i, s, l, e);
SDBUSCPP_ENABLE_NESTED_STRUCT2DICT_SERIALIZATION(my::NestedStruct);
SDBUSCPP_REGISTER_STRUCT(my::NestedStruct, i, s, e, x);
/*-------------------------------------*/
/* -- TEST CASES -- */
/*-------------------------------------*/
@@ -490,7 +520,7 @@ TEST(AMessage, CanCarryDBusArrayGivenAsCustomType)
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryDBusStructGivenAsCustomType)
TEST(AMessage, CanCarryUserDefinedStruct)
{
auto msg = sdbus::createPlainMessage();
@@ -505,6 +535,116 @@ TEST(AMessage, CanCarryDBusStructGivenAsCustomType)
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanCarryNestedUserDefinedStruct)
{
auto msg = sdbus::createPlainMessage();
const my::NestedStruct dataWritten{3545342, "hello"s, my::Enum::Value2, {12, "world"s, {3.14, 2.4568546}, my::Enum::Value3}};
msg << dataWritten;
msg.seal();
my::NestedStruct dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(dataWritten));
}
TEST(AMessage, CanSerializeUserDefinedStructAsDictionaryOfStringsToVariants)
{
auto msg = sdbus::createPlainMessage();
const my::Struct dataWritten{3545342, "hello"s, {3.14, 2.4568546}, my::Enum::Value2};
msg << sdbus::as_dictionary{dataWritten};
msg.seal();
std::map<std::string, sdbus::Variant> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, SizeIs(4));
ASSERT_THAT(dataRead.at("i").get<int>(), Eq(3545342));
ASSERT_THAT(dataRead.at("s").get<std::string>(), Eq("hello"));
ASSERT_THAT(dataRead.at("l").get<std::list<double>>(), ElementsAre(3.14, 2.4568546));
ASSERT_THAT(dataRead.at("e").get<my::Enum>(), Eq(my::Enum::Value2));
}
TEST(AMessage, CanRecursivelySerializeUserDefinedStructAsDictionaryOfStringsToVariants)
{
auto msg = sdbus::createPlainMessage();
const my::NestedStruct dataWritten{3545342, "hello"s, my::Enum::Value2, {12, "world"s, {3.14, 2.4568546}, my::Enum::Value3}};
msg << sdbus::as_dictionary{dataWritten};
msg.seal();
std::map<std::string, sdbus::Variant> dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, SizeIs(4));
ASSERT_THAT(dataRead.at("i").get<int>(), Eq(3545342));
ASSERT_THAT(dataRead.at("s").get<std::string>(), Eq("hello"));
ASSERT_THAT(dataRead.at("e").get<my::Enum>(), Eq(my::Enum::Value2));
auto nestedStructRead = dataRead.at("x").get<std::map<std::string, sdbus::Variant>>(); // Nested struct serialized as dict
ASSERT_THAT(nestedStructRead.at("i").get<int>(), Eq(12));
ASSERT_THAT(nestedStructRead.at("s").get<std::string>(), Eq("world"));
ASSERT_THAT(nestedStructRead.at("l").get<std::list<double>>(), ElementsAre(3.14, 2.4568546));
ASSERT_THAT(nestedStructRead.at("e").get<my::Enum>(), Eq(my::Enum::Value3));
}
TEST(AMessage, CanDeserializeDictionaryOfStringsToVariantsIntoUserDefinedStruct)
{
auto msg = sdbus::createPlainMessage();
std::map<std::string, sdbus::Variant> dataWritten{ {"i", sdbus::Variant{3545342}}
, {"s", sdbus::Variant{"hello"s}}
, {"l", sdbus::Variant{std::list<double>{3.14, 2.4568546}}}
, {"e", sdbus::Variant{my::Enum::Value2}} };
msg << dataWritten;
msg.seal();
my::Struct dataRead;
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(my::Struct{3545342, "hello"s, {3.14, 2.4568546}, my::Enum::Value2}));
}
TEST(AMessage, FailsDeserializingDictionaryIntoUserDefinedStructIfStructMemberIsNotFound)
{
auto msg = sdbus::createPlainMessage();
std::map<std::string, sdbus::Variant> dataWritten{ {"i", sdbus::Variant{3545342}}
, {"nonexistent", sdbus::Variant{"hello"s}}
, {"l", sdbus::Variant{std::list<double>{3.14, 2.4568546}}}
, {"e", sdbus::Variant{my::Enum::Value2}} };
msg << dataWritten;
msg.seal();
my::Struct dataRead;
ASSERT_THROW(msg >> dataRead, sdbus::Error);
}
TEST(AMessage, DeserializesDictionaryIntoStructWithMissingMembersSuccessfullyIfRelaxedOptionIsSet)
{
auto msg = sdbus::createPlainMessage();
std::map<std::string, sdbus::Variant> dataWritten{ {"some_nonexistent_struct_member", sdbus::Variant{3545342}}
, {"another_nonexistent_struct_member", sdbus::Variant{"hello"s}}
, {"l", sdbus::Variant{std::list<double>{3.14, 2.4568546}}}
, {"e", sdbus::Variant{my::Enum::Value2}} };
msg << dataWritten;
msg.seal();
my::RelaxedStruct dataRead{};
msg >> dataRead;
ASSERT_THAT(dataRead, Eq(my::RelaxedStruct{{}, {}, {3.14, 2.4568546}, my::Enum::Value2}));
}
class AMessage : public ::testing::TestWithParam<std::variant<int32_t, std::string, my::Struct>>
{
};