Allow enabling (de)serialization for 3rd party structs
This commit is contained in:
parent
eac2d20638
commit
062f8c0d27
25
README.md
25
README.md
|
@ -140,6 +140,31 @@ So beside the `BOOST_HANA_DEFINE_STRUCT` macro, the usage remains the same.
|
|||
* Inherited members not considered
|
||||
* Support for enums is unlikely
|
||||
|
||||
### Enable reflection for 3rd party classes/structs
|
||||
It is obvious that the previously shown examples do not work for classes
|
||||
defined in 3rd party header files as it requires adding an additional
|
||||
base class.
|
||||
|
||||
To work around this issue, one can use the `REFLECTIVE_RAPIDJSON_MAKE_JSON_SERIALIZABLE`
|
||||
macro. It will enable the `toJson` and `fromJson` methods for the specified class
|
||||
in the `ReflectiveRapidJSON::JsonReflector` namespace:
|
||||
|
||||
```
|
||||
// somewhere in included header
|
||||
struct ThridPartyStruct
|
||||
{ ... };
|
||||
|
||||
// somewhere in own header or source file
|
||||
REFLECTIVE_RAPIDJSON_MAKE_JSON_SERIALIZABLE(ThridPartyStruct)
|
||||
|
||||
// (de)serialization
|
||||
ReflectiveRapidJSON::JsonReflector::toJson(...).GetString();
|
||||
ReflectiveRapidJSON::JsonReflector::fromJson<ThridPartyStruct>("...");
|
||||
```
|
||||
|
||||
The code generator will emit the same code in the same way as `JsonSerializable` was
|
||||
used.
|
||||
|
||||
### Further examples
|
||||
Checkout the test cases for further examples. Relevant files are in
|
||||
the directories `lib/tests` and `generator/tests`.
|
||||
|
|
6
TODOs.md
6
TODOs.md
|
@ -7,10 +7,10 @@
|
|||
- [x] Add documentation (install instructions, usage)
|
||||
- [ ] Support enums (undoable with Boost.Hana)
|
||||
- [ ] Support templated classes
|
||||
- [ ] Allow making 3rdparty classes/structs reflectable
|
||||
- [ ] Add additional parameter for code generator to allow specifying relevant classes
|
||||
- [X] Allow making 3rdparty classes/structs reflectable
|
||||
- [X] Add additional parameter for code generator to allow specifying relevant classes
|
||||
explicitely
|
||||
- [ ] Fix traits currently relying on `JsonSerializable` being base class
|
||||
- [X] Fix traits currently relying on `JsonSerializable` being base class
|
||||
|
||||
## Library-only
|
||||
- [ ] Support `std::unique_ptr` and `std::shared_ptr`
|
||||
|
|
|
@ -54,12 +54,15 @@ include(AppTarget)
|
|||
include(ReflectionGenerator)
|
||||
add_reflection_generator_invocation(
|
||||
INPUT_FILES
|
||||
tests/structs.h # used by test cases
|
||||
tests/cppunit.cpp # just for testing multiple input files and the "empty file" case
|
||||
tests/structs.h # used by test cases
|
||||
tests/cppunit.cpp # just for testing multiple input files and the "empty file" case
|
||||
GENERATORS
|
||||
json
|
||||
OUTPUT_LISTS
|
||||
TEST_HEADER_FILES
|
||||
JSON_CLASSES
|
||||
OtherNotJsonSerializable # test specifying classes for JSON (de)serialization manually
|
||||
SomeOtherClassName # specifying a class that does not exist should not cause any problems
|
||||
)
|
||||
|
||||
# include modules to apply configuration
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "../lib/json/serializable.h"
|
||||
|
||||
#include <clang/AST/DeclCXX.h>
|
||||
#include <clang/AST/DeclTemplate.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
|
@ -31,18 +32,33 @@ ostream &operator<<(ostream &os, llvm::StringRef str)
|
|||
void JsonSerializationCodeGenerator::addDeclaration(clang::Decl *decl)
|
||||
{
|
||||
switch (decl->getKind()) {
|
||||
case clang::Decl::Kind::CXXRecord: {
|
||||
case clang::Decl::Kind::CXXRecord:
|
||||
case clang::Decl::Kind::ClassTemplateSpecialization: {
|
||||
auto *const record = static_cast<clang::CXXRecordDecl *>(decl);
|
||||
// skip forward declarations
|
||||
if (!record->hasDefinition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
string qualifiedName(qualifiedNameIfRelevant(record));
|
||||
if (!qualifiedName.empty()) {
|
||||
m_relevantClasses.emplace_back(move(qualifiedName), record);
|
||||
// check for template specializations to adapt a 3rd party class/struct
|
||||
if (decl->getKind() == clang::Decl::Kind::ClassTemplateSpecialization) {
|
||||
auto *const templRecord = static_cast<clang::ClassTemplateSpecializationDecl *>(decl);
|
||||
if (templRecord->getQualifiedNameAsString() == JsonReflector::AdaptedJsonSerializable<void>::qualifiedName) {
|
||||
const clang::TemplateArgumentList &templateArgs = templRecord->getTemplateArgs();
|
||||
if (templateArgs.size() != 1 || templateArgs.get(0).getKind() != clang::TemplateArgument::Type) {
|
||||
return; // FIXME: use Clang diagnostics to print warning
|
||||
}
|
||||
const clang::CXXRecordDecl *templateRecord = templateArgs.get(0).getAsType()->getAsCXXRecordDecl();
|
||||
if (!templateRecord) {
|
||||
return; // FIXME: use Clang diagnostics to print warning
|
||||
}
|
||||
m_adaptionRecords.emplace_back(templateRecord->getNameAsString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// add any other records
|
||||
m_records.emplace_back(record);
|
||||
}
|
||||
case clang::Decl::Kind::Enum:
|
||||
// TODO: add enums
|
||||
|
@ -53,7 +69,15 @@ void JsonSerializationCodeGenerator::addDeclaration(clang::Decl *decl)
|
|||
|
||||
void JsonSerializationCodeGenerator::generate(ostream &os) const
|
||||
{
|
||||
if (m_relevantClasses.empty()) {
|
||||
// find relevant classes
|
||||
std::vector<RelevantClass> relevantClasses;
|
||||
for (clang::CXXRecordDecl *record : m_records) {
|
||||
string qualifiedName(qualifiedNameIfRelevant(record));
|
||||
if (!qualifiedName.empty()) {
|
||||
relevantClasses.emplace_back(move(qualifiedName), record);
|
||||
}
|
||||
}
|
||||
if (relevantClasses.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -63,12 +87,12 @@ void JsonSerializationCodeGenerator::generate(ostream &os) const
|
|||
|
||||
// add push and pull functions for each class, for an example of the resulting
|
||||
// output, see ../lib/tests/jsonserializable.cpp (code under comment "pretend serialization code...")
|
||||
for (const RelevantClass &relevantClass : m_relevantClasses) {
|
||||
for (const RelevantClass &relevantClass : relevantClasses) {
|
||||
// write comment
|
||||
os << "// define code for (de)serializing " << relevantClass.qualifiedName << " objects\n";
|
||||
|
||||
// find relevant base classes
|
||||
const vector<const RelevantClass *> relevantBases = findRelevantBaseClasses(relevantClass);
|
||||
const vector<const RelevantClass *> relevantBases = findRelevantBaseClasses(relevantClass, relevantClasses);
|
||||
|
||||
// print push method
|
||||
os << "template <> inline void push<::" << relevantClass.qualifiedName << ">(const ::" << relevantClass.qualifiedName
|
||||
|
@ -126,12 +150,19 @@ string JsonSerializationCodeGenerator::qualifiedNameIfRelevant(clang::CXXRecordD
|
|||
return record->getQualifiedNameAsString();
|
||||
}
|
||||
|
||||
// consider all classes for which a specialization of the "AdaptedJsonSerializable" struct is available
|
||||
const string qualifiedName(record->getQualifiedNameAsString());
|
||||
for (const string &adaptionRecord : m_adaptionRecords) {
|
||||
if (adaptionRecord == qualifiedName) {
|
||||
return qualifiedName;
|
||||
}
|
||||
}
|
||||
|
||||
// consider all classes specified via "--additional-classes" argument relevant
|
||||
if (!m_options.additionalClassesArg.isPresent()) {
|
||||
return string();
|
||||
}
|
||||
|
||||
const string qualifiedName(record->getQualifiedNameAsString());
|
||||
for (const char *className : m_options.additionalClassesArg.values()) {
|
||||
if (className == qualifiedName) {
|
||||
return qualifiedName;
|
||||
|
@ -142,10 +173,10 @@ string JsonSerializationCodeGenerator::qualifiedNameIfRelevant(clang::CXXRecordD
|
|||
}
|
||||
|
||||
std::vector<const JsonSerializationCodeGenerator::RelevantClass *> JsonSerializationCodeGenerator::findRelevantBaseClasses(
|
||||
const RelevantClass &relevantClass) const
|
||||
const RelevantClass &relevantClass, const std::vector<RelevantClass> &relevantBases)
|
||||
{
|
||||
vector<const RelevantClass *> relevantBaseClasses;
|
||||
for (const RelevantClass &otherClass : m_relevantClasses) {
|
||||
for (const RelevantClass &otherClass : relevantBases) {
|
||||
if (relevantClass.record != otherClass.record && relevantClass.record->isDerivedFrom(otherClass.record)) {
|
||||
relevantBaseClasses.push_back(&otherClass);
|
||||
}
|
||||
|
|
|
@ -35,9 +35,11 @@ public:
|
|||
std::string qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const;
|
||||
|
||||
private:
|
||||
std::vector<const RelevantClass *> findRelevantBaseClasses(const RelevantClass &relevantClass) const;
|
||||
static std::vector<const RelevantClass *> findRelevantBaseClasses(
|
||||
const RelevantClass &relevantClass, const std::vector<RelevantClass> &relevantBases);
|
||||
|
||||
std::vector<RelevantClass> m_relevantClasses;
|
||||
std::vector<clang::CXXRecordDecl *> m_records;
|
||||
std::vector<std::string> m_adaptionRecords;
|
||||
const Options &m_options;
|
||||
};
|
||||
|
||||
|
|
|
@ -36,7 +36,8 @@ int main(int argc, char *argv[])
|
|||
generatorsArg.setPreDefinedCompletionValues("json");
|
||||
generatorsArg.setRequiredValueCount(Argument::varValueCount);
|
||||
generatorsArg.setCombinable(true);
|
||||
ConfigValueArgument clangOptionsArg("clang-opt", 'c', "specifies an argument to be passed to Clang", { "option" });
|
||||
ConfigValueArgument clangOptionsArg("clang-opt", 'c', "specifies arguments/options to be passed to Clang", { "option" });
|
||||
clangOptionsArg.setRequiredValueCount(Argument::varValueCount);
|
||||
JsonSerializationCodeGenerator::Options jsonOptions;
|
||||
HelpArgument helpArg(parser);
|
||||
NoColorArgument noColorArg;
|
||||
|
|
|
@ -33,6 +33,7 @@ class JsonGeneratorTests : public TestFixture {
|
|||
CPPUNIT_TEST(testSingleInheritence);
|
||||
CPPUNIT_TEST(testMultipleInheritence);
|
||||
CPPUNIT_TEST(testCustomSerialization);
|
||||
CPPUNIT_TEST(test3rdPartyAdaption);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
|
@ -44,6 +45,7 @@ public:
|
|||
void testSingleInheritence();
|
||||
void testMultipleInheritence();
|
||||
void testCustomSerialization();
|
||||
void test3rdPartyAdaption();
|
||||
|
||||
private:
|
||||
const vector<string> m_expectedCode;
|
||||
|
@ -213,6 +215,29 @@ void JsonGeneratorTests::testCustomSerialization()
|
|||
CPPUNIT_ASSERT_EQUAL(test.ts.toString(), parsedTest.ts.toString());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests whether adapting (de)serialization for 3rd party structs works.
|
||||
*/
|
||||
void JsonGeneratorTests::test3rdPartyAdaption()
|
||||
{
|
||||
static_assert(ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable<NotJsonSerializable>::value,
|
||||
"can serialize NotJsonSerializable because of adaption macro");
|
||||
static_assert(!ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable<OtherNotJsonSerializable>::value,
|
||||
"can not serialize OtherNotJsonSerializable because adaption macro missing");
|
||||
static_assert(!ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable<ReallyNotJsonSerializable>::value,
|
||||
"can not serialize ReallyNotJsonSerializable");
|
||||
|
||||
const NotJsonSerializable test;
|
||||
const string str("{\"butSerializableAnyways\":\"useful to adapt 3rd party structs\"}");
|
||||
|
||||
// test serialization
|
||||
CPPUNIT_ASSERT_EQUAL(str, string(ReflectiveRapidJSON::JsonReflector::toJson(test).GetString()));
|
||||
|
||||
// test deserialization
|
||||
const NotJsonSerializable parsedTest(ReflectiveRapidJSON::JsonReflector::fromJson<NotJsonSerializable>(str));
|
||||
CPPUNIT_ASSERT_EQUAL(test.butSerializableAnyways, parsedTest.butSerializableAnyways);
|
||||
}
|
||||
|
||||
// include file required for reflection of TestStruct and other structs defined in structs.h
|
||||
// NOTE: * generation of this header is triggered using the CMake function add_reflection_generator_invocation()
|
||||
// * the include must happen in exactly one translation unit of the project at a point where the structs are defined
|
||||
|
|
|
@ -17,7 +17,7 @@ using namespace ReflectiveRapidJSON;
|
|||
|
||||
/*!
|
||||
* \brief The TestStruct struct inherits from JsonSerializable and should hence have functional fromJson()
|
||||
* and toJson() methods. This is asserted in OverallTests::testIncludingGeneratedHeader();
|
||||
* and toJson() methods. This is asserted in JsonGeneratorTests::testIncludingGeneratedHeader();
|
||||
*/
|
||||
struct TestStruct : public JsonSerializable<TestStruct> {
|
||||
int someInt = 0;
|
||||
|
@ -27,7 +27,7 @@ struct TestStruct : public JsonSerializable<TestStruct> {
|
|||
|
||||
/*!
|
||||
* \brief The NestedTestStruct struct inherits from JsonSerializable and should hence have functional fromJson()
|
||||
* and toJson() methods. This is asserted in OverallTests::testNesting();
|
||||
* and toJson() methods. This is asserted in JsonGeneratorTests::testNesting();
|
||||
*/
|
||||
struct NestedTestStruct : public JsonSerializable<NestedTestStruct> {
|
||||
list<vector<TestStruct>> nested;
|
||||
|
@ -36,7 +36,7 @@ struct NestedTestStruct : public JsonSerializable<NestedTestStruct> {
|
|||
|
||||
/*!
|
||||
* \brief The AnotherTestStruct struct inherits from JsonSerializable and should hence have functional fromJson()
|
||||
* and toJson() methods. This is asserted in OverallTests::testSingleInheritence();
|
||||
* and toJson() methods. This is asserted in JsonGeneratorTests::testSingleInheritence();
|
||||
*/
|
||||
struct AnotherTestStruct : public JsonSerializable<AnotherTestStruct> {
|
||||
vector<string> arrayOfStrings{ "a", "b", "cd" };
|
||||
|
@ -44,7 +44,7 @@ struct AnotherTestStruct : public JsonSerializable<AnotherTestStruct> {
|
|||
|
||||
/*!
|
||||
* \brief The DerivedTestStruct struct inherits from JsonSerializable and should hence have functional fromJson()
|
||||
* and toJson() methods. This is asserted in OverallTests::testInheritence();
|
||||
* and toJson() methods. This is asserted in JsonGeneratorTests::testInheritence();
|
||||
*/
|
||||
struct DerivedTestStruct : public TestStruct, public JsonSerializable<DerivedTestStruct> {
|
||||
bool someBool = true;
|
||||
|
@ -59,7 +59,7 @@ struct NonSerializable {
|
|||
|
||||
/*!
|
||||
* \brief The MultipleDerivedTestStruct struct inherits from JsonSerializable and should hence have functional fromJson()
|
||||
* and toJson() methods. This is asserted in OverallTests::testMultipleInheritence();
|
||||
* and toJson() methods. This is asserted in JsonGeneratorTests::testMultipleInheritence();
|
||||
*/
|
||||
struct MultipleDerivedTestStruct : public TestStruct,
|
||||
public AnotherTestStruct,
|
||||
|
@ -70,11 +70,42 @@ struct MultipleDerivedTestStruct : public TestStruct,
|
|||
|
||||
/*!
|
||||
* \brief The StructWithCustomTypes struct inherits from JsonSerializable and should hence have functional fromJson()
|
||||
* and toJson() methods. This is asserted in OverallTests::testCustomSerialization();
|
||||
* and toJson() methods. This is asserted in JsonGeneratorTests::testCustomSerialization();
|
||||
*/
|
||||
struct StructWithCustomTypes : public JsonSerializable<StructWithCustomTypes> {
|
||||
ChronoUtilities::DateTime dt = ChronoUtilities::DateTime::fromDateAndTime(2017, 4, 2, 15, 31, 21, 165.125);
|
||||
ChronoUtilities::TimeSpan ts = ChronoUtilities::TimeSpan::fromHours(3.25) + ChronoUtilities::TimeSpan::fromSeconds(19.125);
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief The NotJsonSerializable struct is used to tests (de)serialization for 3rd party structs (which do not
|
||||
* inherit from JsonSerializable instance). It is used in JsonGeneratorTests::test3rdPartyAdaption().
|
||||
* \remarks Imagine this struct would have been defined in a 3rd party header.
|
||||
*/
|
||||
struct NotJsonSerializable {
|
||||
std::string butSerializableAnyways = "useful to adapt 3rd party structs";
|
||||
};
|
||||
|
||||
// make "NotJsonSerializable" serializable
|
||||
REFLECTIVE_RAPIDJSON_MAKE_JSON_SERIALIZABLE(NotJsonSerializable);
|
||||
|
||||
/*!
|
||||
* \brief The OtherNotJsonSerializable struct is used to test whether code for (de)serialization is generated for classes explicitely
|
||||
* specified via CMake macro (despite use of REFLECTIVE_RAPIDJSON_ADAPT_JSON_SERIALIZABLE or JsonSerializable is
|
||||
* missing).
|
||||
*/
|
||||
struct OtherNotJsonSerializable {
|
||||
std::string codeIsGenerated = "for this despite missing REFLECTIVE_RAPIDJSON_ADAPT_JSON_SERIALIZABLE";
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief The ReallyNotJsonSerializable struct is used to tests (de)serialization for 3rd party structs (which do not
|
||||
* inherit from JsonSerializable instance). It is used in JsonGeneratorTests::test3rdPartyAdaption().
|
||||
*/
|
||||
struct ReallyNotJsonSerializable {
|
||||
std::string notSerializable;
|
||||
};
|
||||
|
||||
//REFLECTIVE_RAPIDJSON_ADAPT_JSON_SERIALIZABLE(NotJsonSerializable);
|
||||
|
||||
#endif // REFLECTIVE_RAPIDJSON_TESTS_STRUCTS_H
|
||||
|
|
|
@ -25,8 +25,25 @@ namespace ReflectiveRapidJSON {
|
|||
|
||||
template <typename Type> struct JsonSerializable;
|
||||
|
||||
/*!
|
||||
* \brief The JsonReflector namespace contains helper functions to ease the use of RapidJSON for automatic (de)serialization.
|
||||
*/
|
||||
namespace JsonReflector {
|
||||
|
||||
template <typename T> struct AdaptedJsonSerializable : Traits::Bool<false> {
|
||||
static constexpr const char *name = "AdaptedJsonSerializable";
|
||||
static constexpr const char *qualifiedName = "ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable";
|
||||
};
|
||||
|
||||
/*!
|
||||
* \def The REFLECTIVE_RAPIDJSON_MAKE_JSON_SERIALIZABLE macro allows to adapt (de)serialization for types defined in 3rd party header files.
|
||||
* \remarks The struct will not have the toJson() and fromJson() methods available. Use the corresponding functions in the namespace
|
||||
* ReflectiveRapidJSON::JsonReflector instead.
|
||||
*/
|
||||
#define REFLECTIVE_RAPIDJSON_MAKE_JSON_SERIALIZABLE(T) \
|
||||
template <> struct ::ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable<T> : Traits::Bool<true> { \
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Casts the specified \a size to the size type used by RapidJSON ensuring no overflow happens.
|
||||
*/
|
||||
|
@ -481,7 +498,7 @@ inline void pull(Type &reflectable, const char *name, const rapidjson::GenericVa
|
|||
/*!
|
||||
* \brief Serializes the specified \a reflectable which has a custom type.
|
||||
*/
|
||||
template <typename Type, Traits::EnableIfAny<std::is_base_of<JsonSerializable<Type>, Type>>...>
|
||||
template <typename Type, Traits::EnableIfAny<std::is_base_of<JsonSerializable<Type>, Type>, AdaptedJsonSerializable<Type>>...>
|
||||
RAPIDJSON_NAMESPACE::StringBuffer toJson(const Type &reflectable)
|
||||
{
|
||||
RAPIDJSON_NAMESPACE::Document document(RAPIDJSON_NAMESPACE::kObjectType);
|
||||
|
@ -527,7 +544,7 @@ template <typename Type, Traits::EnableIfAny<std::is_same<Type, const char *>>..
|
|||
/*!
|
||||
* \brief Deserializes the specified JSON to \tparam Type which is a custom type.
|
||||
*/
|
||||
template <typename Type, Traits::EnableIfAny<std::is_base_of<JsonSerializable<Type>, Type>>...>
|
||||
template <typename Type, Traits::EnableIfAny<std::is_base_of<JsonSerializable<Type>, Type>, AdaptedJsonSerializable<Type>>...>
|
||||
Type fromJson(const char *json, std::size_t jsonSize, JsonDeserializationErrors *errors = nullptr)
|
||||
{
|
||||
RAPIDJSON_NAMESPACE::Document doc(parseJsonDocFromString(json, jsonSize));
|
||||
|
|
Loading…
Reference in New Issue