diff --git a/README.md b/README.md index ced06d3..74382aa 100644 --- a/README.md +++ b/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("..."); +``` + +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`. diff --git a/TODOs.md b/TODOs.md index 7733fcc..abc59b2 100644 --- a/TODOs.md +++ b/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` diff --git a/generator/CMakeLists.txt b/generator/CMakeLists.txt index c0899de..07977ea 100644 --- a/generator/CMakeLists.txt +++ b/generator/CMakeLists.txt @@ -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 diff --git a/generator/jsonserializationcodegenerator.cpp b/generator/jsonserializationcodegenerator.cpp index f2113e3..423bcd1 100644 --- a/generator/jsonserializationcodegenerator.cpp +++ b/generator/jsonserializationcodegenerator.cpp @@ -3,6 +3,7 @@ #include "../lib/json/serializable.h" #include +#include #include @@ -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(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(decl); + if (templRecord->getQualifiedNameAsString() == JsonReflector::AdaptedJsonSerializable::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 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 relevantBases = findRelevantBaseClasses(relevantClass); + const vector 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 JsonSerializationCodeGenerator::findRelevantBaseClasses( - const RelevantClass &relevantClass) const + const RelevantClass &relevantClass, const std::vector &relevantBases) { vector 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); } diff --git a/generator/jsonserializationcodegenerator.h b/generator/jsonserializationcodegenerator.h index 89303a7..a011cb1 100644 --- a/generator/jsonserializationcodegenerator.h +++ b/generator/jsonserializationcodegenerator.h @@ -35,9 +35,11 @@ public: std::string qualifiedNameIfRelevant(clang::CXXRecordDecl *record) const; private: - std::vector findRelevantBaseClasses(const RelevantClass &relevantClass) const; + static std::vector findRelevantBaseClasses( + const RelevantClass &relevantClass, const std::vector &relevantBases); - std::vector m_relevantClasses; + std::vector m_records; + std::vector m_adaptionRecords; const Options &m_options; }; diff --git a/generator/main.cpp b/generator/main.cpp index 3e27259..8e2eeee 100644 --- a/generator/main.cpp +++ b/generator/main.cpp @@ -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; diff --git a/generator/tests/jsongenerator.cpp b/generator/tests/jsongenerator.cpp index 1e76ec4..213bb92 100644 --- a/generator/tests/jsongenerator.cpp +++ b/generator/tests/jsongenerator.cpp @@ -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 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::value, + "can serialize NotJsonSerializable because of adaption macro"); + static_assert(!ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable::value, + "can not serialize OtherNotJsonSerializable because adaption macro missing"); + static_assert(!ReflectiveRapidJSON::JsonReflector::AdaptedJsonSerializable::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(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 diff --git a/generator/tests/structs.h b/generator/tests/structs.h index e8708db..6588cd9 100644 --- a/generator/tests/structs.h +++ b/generator/tests/structs.h @@ -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 { int someInt = 0; @@ -27,7 +27,7 @@ struct TestStruct : public JsonSerializable { /*! * \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 { list> nested; @@ -36,7 +36,7 @@ struct NestedTestStruct : public JsonSerializable { /*! * \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 { vector arrayOfStrings{ "a", "b", "cd" }; @@ -44,7 +44,7 @@ struct AnotherTestStruct : public JsonSerializable { /*! * \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 { 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 { 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 diff --git a/lib/json/reflector.h b/lib/json/reflector.h index 1712ca5..b904f71 100644 --- a/lib/json/reflector.h +++ b/lib/json/reflector.h @@ -25,8 +25,25 @@ namespace ReflectiveRapidJSON { template struct JsonSerializable; +/*! + * \brief The JsonReflector namespace contains helper functions to ease the use of RapidJSON for automatic (de)serialization. + */ namespace JsonReflector { +template struct AdaptedJsonSerializable : Traits::Bool { + 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 : Traits::Bool { \ + } + /*! * \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 , Type>>...> +template , Type>, AdaptedJsonSerializable>...> RAPIDJSON_NAMESPACE::StringBuffer toJson(const Type &reflectable) { RAPIDJSON_NAMESPACE::Document document(RAPIDJSON_NAMESPACE::kObjectType); @@ -527,7 +544,7 @@ template >.. /*! * \brief Deserializes the specified JSON to \tparam Type which is a custom type. */ -template , Type>>...> +template , Type>, AdaptedJsonSerializable>...> Type fromJson(const char *json, std::size_t jsonSize, JsonDeserializationErrors *errors = nullptr) { RAPIDJSON_NAMESPACE::Document doc(parseJsonDocFromString(json, jsonSize));