Allow enabling (de)serialization for 3rd party structs

This commit is contained in:
Martchus 2017-11-06 21:25:45 +01:00
parent eac2d20638
commit 062f8c0d27
9 changed files with 162 additions and 27 deletions

View File

@ -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`.

View File

@ -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`

View File

@ -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

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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));