diff --git a/generator/codegenerator.cpp b/generator/codegenerator.cpp index 3b3efa8..2a87cfb 100644 --- a/generator/codegenerator.cpp +++ b/generator/codegenerator.cpp @@ -92,10 +92,12 @@ void JSONSerializationCodeGenerator::generate(ostream &os) const // print push method os << "template <> inline void push<::" << relevantClass.qualifiedName << ">(const ::" << relevantClass.qualifiedName - << " &reflectable, ::RAPIDJSON_NAMESPACE::Value::Object &value, ::RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)\n{\n"; + << " &reflectable, ::RAPIDJSON_NAMESPACE::Value::Object &value, ::RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)\n{\n" + " // push base classes\n"; for (const RelevantClass *baseClass : relevantBases) { os << " push(static_castqualifiedName << " &>(reflectable), value, allocator);\n"; } + os << " // push members\n"; for (const clang::FieldDecl *field : relevantClass.record->fields()) { os << " push(reflectable." << field->getName() << ", \"" << field->getName() << "\", value, allocator);\n"; } @@ -103,13 +105,28 @@ void JSONSerializationCodeGenerator::generate(ostream &os) const // print pull method os << "template <> inline void pull<::" << relevantClass.qualifiedName << ">(::" << relevantClass.qualifiedName - << " &reflectable, const ::RAPIDJSON_NAMESPACE::GenericValue<::RAPIDJSON_NAMESPACE::UTF8>::ConstObject &value)\n{\n"; + << " &reflectable, const ::RAPIDJSON_NAMESPACE::GenericValue<::RAPIDJSON_NAMESPACE::UTF8>::ConstObject &value, JSONParseErrors " + "*errors)\n{\n" + " // pull base classes\n"; for (const RelevantClass *baseClass : relevantBases) { - os << " pull(static_cast<::" << baseClass->qualifiedName << " &>(reflectable), value);\n"; + os << " pull(static_cast<::" << baseClass->qualifiedName << " &>(reflectable), value, errors);\n"; } + os << " // set error context for current record\n" + " const char *previousRecord;\n" + " if (errors) {\n" + " previousRecord = errors->currentRecord;\n" + " errors->currentRecord = \"" + << relevantClass.qualifiedName + << "\";\n" + " }\n" + " // pull members\n"; for (const clang::FieldDecl *field : relevantClass.record->fields()) { - os << " pull(reflectable." << field->getName() << ", \"" << field->getName() << "\", value);\n"; + os << " pull(reflectable." << field->getName() << ", \"" << field->getName() << "\", value, errors);\n"; } + os << " // restore error context for previous record\n" + " if (errors) {\n" + " errors->currentRecord = previousRecord;\n" + " }\n"; os << "}\n\n"; } diff --git a/generator/testfiles/some_structs_json_serialization.h b/generator/testfiles/some_structs_json_serialization.h index 75b8e61..c4b009b 100644 --- a/generator/testfiles/some_structs_json_serialization.h +++ b/generator/testfiles/some_structs_json_serialization.h @@ -4,13 +4,27 @@ namespace Reflector { // define code for (de)serializing TestNamespace1::Person objects template <> inline void push<::TestNamespace1::Person>(const ::TestNamespace1::Person &reflectable, ::RAPIDJSON_NAMESPACE::Value::Object &value, ::RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { + // push base classes + // push members push(reflectable.age, "age", value, allocator); push(reflectable.alive, "alive", value, allocator); } -template <> inline void pull<::TestNamespace1::Person>(::TestNamespace1::Person &reflectable, const ::RAPIDJSON_NAMESPACE::GenericValue<::RAPIDJSON_NAMESPACE::UTF8>::ConstObject &value) +template <> inline void pull<::TestNamespace1::Person>(::TestNamespace1::Person &reflectable, const ::RAPIDJSON_NAMESPACE::GenericValue<::RAPIDJSON_NAMESPACE::UTF8>::ConstObject &value, JSONParseErrors *errors) { - pull(reflectable.age, "age", value); - pull(reflectable.alive, "alive", value); + // pull base classes + // set error context for current record + const char *previousRecord; + if (errors) { + previousRecord = errors->currentRecord; + errors->currentRecord = "TestNamespace1::Person"; + } + // pull members + pull(reflectable.age, "age", value, errors); + pull(reflectable.alive, "alive", value, errors); + // restore error context for previous record + if (errors) { + errors->currentRecord = previousRecord; + } } } // namespace Reflector diff --git a/generator/tests/jsongenerator.cpp b/generator/tests/jsongenerator.cpp index 5d16969..8b9dac4 100644 --- a/generator/tests/jsongenerator.cpp +++ b/generator/tests/jsongenerator.cpp @@ -52,7 +52,7 @@ CPPUNIT_TEST_SUITE_REGISTRATION(OverallTests); void OverallTests::setUp() { - m_expectedCode = toArrayOfLines(readFile(testFilePath("some_structs_json_serialization.h"), 1024)); + m_expectedCode = toArrayOfLines(readFile(testFilePath("some_structs_json_serialization.h"), 2 * 1024)); } void OverallTests::tearDown() diff --git a/lib/jsonreflector-boosthana.h b/lib/jsonreflector-boosthana.h index 190c4b0..c56e7a4 100644 --- a/lib/jsonreflector-boosthana.h +++ b/lib/jsonreflector-boosthana.h @@ -5,6 +5,11 @@ * \file jsonreflector-boosthana.h * \brief Contains generic functions relying on Boost.Hana which can replace the code which would * otherwise had to be generated. + * \remarks + * These functions use boost::hana::keys() and boost::hana::at_key() rather than the "plain" + * for-loop shown in the introspection examples of the Boost.Hana documentation. The reason is that + * the "plain" for-loop involves making copies. This costs performance and - more importantly - prevents + * modifying the actual object. */ #include "./jsonreflector.h" @@ -22,8 +27,8 @@ template , Traits::Not>>>...> void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value::Object &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { - boost::hana::for_each(reflectable, [&value, &allocator](auto pair) { - push(boost::hana::second(pair), boost::hana::to(boost::hana::first(pair)), value, allocator); + boost::hana::for_each(boost::hana::keys(reflectable), [&reflectable, &value, &allocator](auto key) { + push(boost::hana::at_key(reflectable, key), boost::hana::to(key), value, allocator); }); } @@ -32,19 +37,21 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value::Object &value, RA template , std::is_floating_point, std::is_pointer, Traits::All, Traits::Not>>>...> -void pull(Type &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value) +void pull(Type &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value, JSONParseErrors *errors) { - boost::hana::for_each( - reflectable, [&value](auto pair) { pull(boost::hana::second(pair), boost::hana::to(boost::hana::first(pair)), value); }); + boost::hana::for_each(boost::hana::keys(reflectable), [&reflectable, &value, &errors](auto key) { + pull(boost::hana::at_key(reflectable, key), boost::hana::to(key), value, errors); + }); } template , std::is_floating_point, std::is_pointer, Traits::All, Traits::Not>>>...> -void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue>::ConstObject &value) +void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue>::ConstObject &value, JSONParseErrors *errors) { - boost::hana::for_each( - reflectable, [&value](auto pair) { pull(boost::hana::second(pair), boost::hana::to(boost::hana::first(pair)), value); }); + boost::hana::for_each(boost::hana::keys(reflectable), [&reflectable, &value, &errors](auto key) { + pull(boost::hana::at_key(reflectable, key), boost::hana::to(key), value, errors); + }); } } // namespace Reflector diff --git a/lib/jsonreflector.h b/lib/jsonreflector.h index 4e14a24..a0ab8b0 100644 --- a/lib/jsonreflector.h +++ b/lib/jsonreflector.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -21,20 +22,175 @@ namespace ReflectiveRapidJSON { -enum class ErrorFlags : unsigned char { TypeMismatch, MemberMissing }; - -constexpr ErrorFlags operator&(ErrorFlags lhs, ErrorFlags rhs) -{ - return static_cast(static_cast(lhs) & static_cast(rhs)); -} - -constexpr ErrorFlags operator|(ErrorFlags lhs, ErrorFlags rhs) -{ - return static_cast(static_cast(lhs) | static_cast(rhs)); -} - template struct JSONSerializable; +/*! + * \brief The JSONParseErrorKind enum specifies which kind of error happend when populating variables from parsing results. + */ +enum class JSONParseErrorKind : byte { + TypeMismatch, +}; + +/*! + * \brief The JSONType enum specifies the JSON data type. + * \remarks This is currently only used for error handling to propagate expected and actual types in case of a mismatch. + */ +enum class JSONType : byte { + Null, + Number, + Bool, + String, + Array, + Object, +}; + +template >, Traits::Any, std::is_floating_point>>...> +constexpr JSONType jsonType() +{ + return JSONType::Number; +} + +template >...> constexpr JSONType jsonType() +{ + return JSONType::Bool; +} + +template , Traits::IsCString>...> constexpr JSONType jsonType() +{ + return JSONType::String; +} + +template , Traits::Not>>...> constexpr JSONType jsonType() +{ + return JSONType::Array; +} + +template , std::is_floating_point, Traits::IsString, Traits::IsCString, + Traits::IsIteratable>...> +constexpr JSONType jsonType() +{ + return JSONType::Object; +} + +/*! + * \brief Maps the type info provided by RapidJSON to JSONType. + */ +constexpr JSONType jsonType(RAPIDJSON_NAMESPACE::Type type) +{ + switch (type) { + case RAPIDJSON_NAMESPACE::kNullType: + return JSONType::Null; + case RAPIDJSON_NAMESPACE::kFalseType: + case RAPIDJSON_NAMESPACE::kTrueType: + return JSONType::Bool; + case RAPIDJSON_NAMESPACE::kObjectType: + return JSONType::Object; + case RAPIDJSON_NAMESPACE::kArrayType: + return JSONType::Array; + case RAPIDJSON_NAMESPACE::kStringType: + return JSONType::String; + case RAPIDJSON_NAMESPACE::kNumberType: + return JSONType::Number; + default: + return JSONType::Null; + } +} + +/*! + * \brief The JSONParseError struct describes any errors of fromJson() except such caused by invalid JSON. + */ +struct JSONParseError { + JSONParseError(JSONParseErrorKind kind, JSONType expectedType, JSONType actualType, const char *record, const char *member = nullptr, + std::size_t index = noIndex); + + /// \brief Which kind of error occured. + JSONParseErrorKind kind; + /// \brief The expected type (might not be relevant for all error kinds). + JSONType expectedType; + /// \brief The actual type (might not be relevant for all error kinds). + JSONType actualType; + /// \brief The name of the class or struct which was being processed when the error was ascertained. + const char *record; + /// \brief The name of the member which was being processed when the error was ascertained. + const char *member; + /// \brief The index in the array which was being processed when the error was ascertained. + std::size_t index; + + /// \brief Indicates no array was being processed when the error occured. + static constexpr std::size_t noIndex = std::numeric_limits::max(); +}; + +/*! + * \brief Constructs a new JSONParseError. + * \remarks Supposed to be called by JSONParseErrors::reportTypeMismatch() and similar methods of JSONParseErrors. + */ +inline JSONParseError::JSONParseError( + JSONParseErrorKind kind, JSONType expectedType, JSONType actualType, const char *record, const char *member, std::size_t index) + : kind(kind) + , expectedType(expectedType) + , actualType(actualType) + , record(record) + , member(member) + , index(index) +{ +} + +/*! + * \brief The JSONParseErrors struct can be passed to fromJson() for error handling. + * + * When passed to fromJson() and an error occurs, a JSONParseError is added to this object. + * If throwOn is set, the JSONParseError is additionally thrown making the error fatal. + * + * \remarks Errors due to invalid JSON always lead to a RAPIDJSON_NAMESPACE::ParseResult object being thrown. So this + * only concerns errors listed in JSONParseErrorKind. + */ +struct JSONParseErrors : public std::vector { + JSONParseErrors(); + + template void reportTypeMismatch(RAPIDJSON_NAMESPACE::Type presentType); + + /// \brief The name of the class or struct which is currently being processed. + const char *currentRecord; + /// \brief The name of the member (in currentRecord) which is currently being processed. + const char *currentMember; + /// \brief The index in the array which is currently processed. + std::size_t currentIndex; + /// \brief The list of fatal error types in form of flags. + enum class ThrowOn : unsigned char { None = 0, TypeMismatch = 0x1 } throwOn; +}; + +/*! + * \brief Creates an empty JSONParseErrors object with default context and no errors considered fatal. + */ +inline JSONParseErrors::JSONParseErrors() + : currentRecord("[document]") + , currentMember(nullptr) + , currentIndex(JSONParseError::noIndex) + , throwOn(ThrowOn::None) +{ +} + +/*! + * \brief Combines to ThrowOn values. + */ +constexpr JSONParseErrors::ThrowOn operator|(JSONParseErrors::ThrowOn lhs, JSONParseErrors::ThrowOn rhs) +{ + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +/*! + * \brief Reports a type missmatch between \tparam ExpectedType and \a presentType within the current context. + */ +template inline void JSONParseErrors::reportTypeMismatch(RAPIDJSON_NAMESPACE::Type presentType) +{ + emplace_back(JSONParseErrorKind::TypeMismatch, jsonType(), jsonType(presentType), currentRecord, currentMember, currentIndex); + if (static_cast(throwOn) & static_cast(ThrowOn::TypeMismatch)) { + throw back(); + } +} + inline RAPIDJSON_NAMESPACE::StringBuffer documentToString(RAPIDJSON_NAMESPACE::Document &document) { RAPIDJSON_NAMESPACE::StringBuffer buffer; @@ -190,132 +346,190 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value::Array &value, RAP template , std::is_floating_point, std::is_pointer, Traits::All, Traits::Not>>>...> -void pull(Type &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value); +void pull(Type &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value, JSONParseErrors *errors); template , std::is_floating_point, std::is_pointer, Traits::All, Traits::Not>>>...> -void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue>::ConstObject &value); +void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue>::ConstObject &value, JSONParseErrors *errors); template , std::is_floating_point, std::is_pointer, Traits::All, Traits::Not>>>...> -void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue> &value) +void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue> &value, JSONParseErrors *errors) { if (!value.IsObject()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value.GetType()); + } + return; } - pull(reflectable, value.GetObject()); + pull(reflectable, value.GetObject(), errors); } template , std::is_floating_point, std::is_pointer>...> -inline void pull(Type &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value) +inline void pull(Type &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value, JSONParseErrors *errors) { if (!value->Is()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value->GetType()); + } + return; } reflectable = value->Get(); ++value; } template , std::is_floating_point, std::is_pointer>...> -inline void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue> &value) +inline void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue> &value, JSONParseErrors *errors) { if (!value.Is()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value.GetType()); + } + return; } reflectable = value.Get(); } template <> -inline void pull(std::string &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value) +inline void pull( + std::string &reflectable, RAPIDJSON_NAMESPACE::GenericValue>::ValueIterator &value, JSONParseErrors *errors) { if (!value->IsString()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value->GetType()); + } + return; } reflectable = value->GetString(); ++value; } -template <> inline void pull(std::string &reflectable, const RAPIDJSON_NAMESPACE::GenericValue> &value) +template <> +inline void pull( + std::string &reflectable, const RAPIDJSON_NAMESPACE::GenericValue> &value, JSONParseErrors *errors) { if (!value.IsString()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value.GetType()); + } + return; } reflectable = value.GetString(); } template , Traits::Not>>...> -void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array); +void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JSONParseErrors *errors); template , Traits::Not>, Traits::Not>>...> -void pull(Type &reflectable, rapidjson::GenericValue>::ValueIterator &value) +void pull(Type &reflectable, rapidjson::GenericValue>::ValueIterator &value, JSONParseErrors *errors) { if (!value->IsArray()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value->GetType()); + } + return; } - pull(reflectable, value->GetArray()); + pull(reflectable, value->GetArray(), errors); ++value; } template , Traits::IsReservable, Traits::Not>>...> -void pull(Type &reflectable, rapidjson::GenericValue>::ValueIterator &value) +void pull(Type &reflectable, rapidjson::GenericValue>::ValueIterator &value, JSONParseErrors *errors) { if (!value->IsArray()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value->GetType()); + } + return; } auto array = value->GetArray(); reflectable.reserve(array.Size()); - pull(reflectable, array); + pull(reflectable, array, errors); ++value; } template , Traits::Not>, Traits::Not>>...> -void pull(Type &reflectable, const rapidjson::GenericValue> &value) +void pull(Type &reflectable, const rapidjson::GenericValue> &value, JSONParseErrors *errors) { if (!value.IsArray()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value.GetType()); + } + return; } - pull(reflectable, value.GetArray()); + pull(reflectable, value.GetArray(), errors); } template , Traits::IsReservable, Traits::Not>>...> -void pull(Type &reflectable, const rapidjson::GenericValue> &value) +void pull(Type &reflectable, const rapidjson::GenericValue> &value, JSONParseErrors *errors) { if (!value.IsArray()) { - return; // TODO: handle type mismatch + if (errors) { + errors->reportTypeMismatch(value.GetType()); + } + return; } auto array = value.GetArray(); reflectable.reserve(array.Size()); - pull(reflectable, array); + pull(reflectable, array, errors); } template , Traits::Not>>...> -void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array) +void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JSONParseErrors *errors) { - // clear previous contents of the array (TODO: make this configurable) + // clear previous contents of the array reflectable.clear(); + // pull all array elements of the specified value + std::size_t index = 0; for (const rapidjson::GenericValue> &item : array) { + // set error context for current index + if (errors) { + errors->currentIndex = index; + } reflectable.emplace_back(); - pull(reflectable.back(), item); + pull(reflectable.back(), item, errors); + ++index; + } + + // clear error context + if (errors) { + errors->currentIndex = JSONParseError::noIndex; } } template -inline void pull(Type &reflectable, const char *name, const rapidjson::GenericValue>::ConstObject &value) +inline void pull( + Type &reflectable, const char *name, const rapidjson::GenericValue>::ConstObject &value, JSONParseErrors *errors) { + // find member auto member = value.FindMember(name); if (member == value.MemberEnd()) { return; // TODO: handle member missing } - pull(reflectable, value.FindMember(name)->value); + + // set error context for current member + const char *previousMember; + if (errors) { + previousMember = errors->currentMember; + errors->currentMember = name; + } + + // actually pull value for member + pull(reflectable, value.FindMember(name)->value, errors); + + // restore previous error context + if (errors) { + errors->currentMember = previousMember; + } } // define functions providing high-level JSON serialization @@ -354,33 +568,44 @@ template >.. // define functions providing high-level JSON deserialization -template , Type>>...> Type fromJson(const char *json, std::size_t jsonSize) +template , Type>>...> +Type fromJson(const char *json, std::size_t jsonSize, JSONParseErrors *errors = nullptr) { RAPIDJSON_NAMESPACE::Document doc(parseDocumentFromString(json, jsonSize)); if (!doc.IsObject()) { + if (errors) { + errors->reportTypeMismatch(doc.GetType()); + } return Type(); } Type res; - pull(res, doc.GetObject()); + pull(res, doc.GetObject(), errors); return res; } template , std::is_floating_point>...> -Type fromJson(const char *json, std::size_t jsonSize) +Type fromJson(const char *json, std::size_t jsonSize, JSONParseErrors *errors) { RAPIDJSON_NAMESPACE::Document doc(parseDocumentFromString(json, jsonSize)); if (!doc.Is()) { + if (errors) { + errors->reportTypeMismatch(doc.GetType()); + } return Type(); } return doc.Get(); } -template >...> Type fromJson(const char *json, std::size_t jsonSize) +template >...> +Type fromJson(const char *json, std::size_t jsonSize, JSONParseErrors *errors) { RAPIDJSON_NAMESPACE::Document doc(parseDocumentFromString(json, jsonSize)); if (!doc.IsString()) { + if (errors) { + errors->reportTypeMismatch(doc.GetType()); + } return Type(); } diff --git a/lib/jsonserializable.h b/lib/jsonserializable.h index 3850351..0c8c64c 100644 --- a/lib/jsonserializable.h +++ b/lib/jsonserializable.h @@ -25,9 +25,9 @@ template struct JSONSerializable { // high-level API RAPIDJSON_NAMESPACE::StringBuffer toJson() const; - static Type fromJson(const char *json, std::size_t jsonSize); - static Type fromJson(const char *json); - static Type fromJson(const std::string &json); + static Type fromJson(const char *json, std::size_t jsonSize, JSONParseErrors *errors = nullptr); + static Type fromJson(const char *json, JSONParseErrors *errors = nullptr); + static Type fromJson(const std::string &json, JSONParseErrors *errors = nullptr); static constexpr const char *qualifiedName = "ReflectiveRapidJSON::JSONSerializable"; }; @@ -60,25 +60,25 @@ template RAPIDJSON_NAMESPACE::StringBuffer JSONSerializable Type JSONSerializable::fromJson(const char *json, std::size_t jsonSize) +template Type JSONSerializable::fromJson(const char *json, std::size_t jsonSize, JSONParseErrors *errors) { - return Reflector::fromJson(json, jsonSize); + return Reflector::fromJson(json, jsonSize, errors); } /*! * \brief Constructs a new object from the specified JSON. */ -template Type JSONSerializable::fromJson(const char *json) +template Type JSONSerializable::fromJson(const char *json, JSONParseErrors *errors) { - return Reflector::fromJson(json, std::strlen(json)); + return Reflector::fromJson(json, std::strlen(json), errors); } /*! * \brief Constructs a new object from the specified JSON. */ -template Type JSONSerializable::fromJson(const std::string &json) +template Type JSONSerializable::fromJson(const std::string &json, JSONParseErrors *errors) { - return Reflector::fromJson(json.data(), json.size()); + return Reflector::fromJson(json.data(), json.size(), errors); } /*! diff --git a/lib/tests/jsonreflector-boosthana.cpp b/lib/tests/jsonreflector-boosthana.cpp index 88297b2..abfb6ba 100644 --- a/lib/tests/jsonreflector-boosthana.cpp +++ b/lib/tests/jsonreflector-boosthana.cpp @@ -30,16 +30,21 @@ using namespace ReflectiveRapidJSON; /// \cond // define some structs for testing serialization -struct TestObject : public JSONSerializable { - BOOST_HANA_DEFINE_STRUCT(TestObject, (int, number), (double, number2), (vector, numbers), (string, text), (bool, boolean)); +struct TestObjectHana : public JSONSerializable { + //TestObjectHana(){}; + //TestObjectHana(const TestObjectHana &) + //{ + // std::cout << "copied!!" << std::endl; + //}; + BOOST_HANA_DEFINE_STRUCT(TestObjectHana, (int, number), (double, number2), (vector, numbers), (string, text), (bool, boolean)); }; -struct NestingObject : public JSONSerializable { - BOOST_HANA_DEFINE_STRUCT(NestingObject, (string, name), (TestObject, testObj)); +struct NestingObjectHana : public JSONSerializable { + BOOST_HANA_DEFINE_STRUCT(NestingObjectHana, (string, name), (TestObjectHana, testObj)); }; -struct NestingArray : public JSONSerializable { - BOOST_HANA_DEFINE_STRUCT(NestingArray, (string, name), (vector, testObjects)); +struct NestingArrayHana : public JSONSerializable { + BOOST_HANA_DEFINE_STRUCT(NestingArrayHana, (string, name), (vector, testObjects)); }; /// \endcond @@ -56,6 +61,7 @@ class JSONReflectorBoostHanaTests : public TestFixture { CPPUNIT_TEST(testDeserializePrimitives); CPPUNIT_TEST(testDeserializeSimpleObjects); CPPUNIT_TEST(testDeserializeNestedObjects); + CPPUNIT_TEST(testHandlingTypeMismatch); CPPUNIT_TEST_SUITE_END(); public: @@ -68,6 +74,7 @@ public: void testDeserializePrimitives(); void testDeserializeSimpleObjects(); void testDeserializeNestedObjects(); + void testHandlingTypeMismatch(); private: }; @@ -118,7 +125,7 @@ void JSONReflectorBoostHanaTests::testSerializePrimitives() */ void JSONReflectorBoostHanaTests::testSerializeSimpleObjects() { - TestObject testObj; + TestObjectHana testObj; testObj.number = 42; testObj.number2 = 3.141592653589793; testObj.numbers = { 1, 2, 3, 4 }; @@ -133,9 +140,9 @@ void JSONReflectorBoostHanaTests::testSerializeSimpleObjects() */ void JSONReflectorBoostHanaTests::testSerializeNestedObjects() { - NestingObject nestingObj; + NestingObjectHana nestingObj; nestingObj.name = "nesting"; - TestObject &testObj = nestingObj.testObj; + TestObjectHana &testObj = nestingObj.testObj; testObj.number = 42; testObj.number2 = 3.141592653589793; testObj.numbers = { 1, 2, 3, 4 }; @@ -144,7 +151,7 @@ void JSONReflectorBoostHanaTests::testSerializeNestedObjects() CPPUNIT_ASSERT_EQUAL( "{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}}"s, string(nestingObj.toJson().GetString())); - NestingArray nestingArray; + NestingArrayHana nestingArray; nestingArray.name = "nesting2"; nestingArray.testObjects.emplace_back(testObj); nestingArray.testObjects.emplace_back(testObj); @@ -169,13 +176,14 @@ void JSONReflectorBoostHanaTests::testDeserializePrimitives() bool bool1 = false, bool2 = true; float float1 = 0.0; double double1 = 0.0; - Reflector::pull(str1, array); - Reflector::pull(int1, array); - Reflector::pull(float1, array); - Reflector::pull(str2, array); - Reflector::pull(bool1, array); - Reflector::pull(double1, array); - Reflector::pull(bool2, array); + JSONParseErrors errors; + Reflector::pull(str1, array, &errors); + Reflector::pull(int1, array, &errors); + Reflector::pull(float1, array, &errors); + Reflector::pull(str2, array, &errors); + Reflector::pull(bool1, array, &errors); + Reflector::pull(double1, array, &errors); + Reflector::pull(bool2, array, &errors); CPPUNIT_ASSERT_EQUAL("a"s, str1); CPPUNIT_ASSERT_EQUAL(5, int1); @@ -191,8 +199,8 @@ void JSONReflectorBoostHanaTests::testDeserializePrimitives() */ void JSONReflectorBoostHanaTests::testDeserializeSimpleObjects() { - const TestObject testObj( - TestObject::fromJson("{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}")); + const TestObjectHana testObj( + TestObjectHana::fromJson("{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}")); CPPUNIT_ASSERT_EQUAL(42, testObj.number); CPPUNIT_ASSERT_EQUAL(3.141592653589793, testObj.number2); @@ -206,9 +214,9 @@ void JSONReflectorBoostHanaTests::testDeserializeSimpleObjects() */ void JSONReflectorBoostHanaTests::testDeserializeNestedObjects() { - const NestingObject nestingObj(NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793," - "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}}")); - const TestObject &testObj = nestingObj.testObj; + const NestingObjectHana nestingObj(NestingObjectHana::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793," + "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}}")); + const TestObjectHana &testObj = nestingObj.testObj; CPPUNIT_ASSERT_EQUAL("nesting"s, nestingObj.name); CPPUNIT_ASSERT_EQUAL(42, testObj.number); CPPUNIT_ASSERT_EQUAL(3.141592653589793, testObj.number2); @@ -216,18 +224,97 @@ void JSONReflectorBoostHanaTests::testDeserializeNestedObjects() CPPUNIT_ASSERT_EQUAL("test"s, testObj.text); CPPUNIT_ASSERT_EQUAL(false, testObj.boolean); - const NestingArray nestingArray(NestingArray::fromJson("{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793," - "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false},{\"number\":43,\"number2\":3." - "141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}]}")); - const vector &testObjects = nestingArray.testObjects; + const NestingArrayHana nestingArray( + NestingArrayHana::fromJson("{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793," + "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false},{\"number\":43,\"number2\":3." + "141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}]}")); + const vector &testObjects = nestingArray.testObjects; CPPUNIT_ASSERT_EQUAL("nesting2"s, nestingArray.name); CPPUNIT_ASSERT_EQUAL(2_st, testObjects.size()); CPPUNIT_ASSERT_EQUAL(42, testObjects[0].number); CPPUNIT_ASSERT_EQUAL(43, testObjects[1].number); - for (const TestObject &testObj : testObjects) { + for (const TestObjectHana &testObj : testObjects) { CPPUNIT_ASSERT_EQUAL(3.141592653589793, testObj.number2); CPPUNIT_ASSERT_EQUAL(vector({ 1, 2, 3, 4 }), testObj.numbers); CPPUNIT_ASSERT_EQUAL("test"s, testObj.text); CPPUNIT_ASSERT_EQUAL(false, testObj.boolean); } } + +/*! + * \brief Tests whether JSONParseError is thrown on type mismatch. + */ +void JSONReflectorBoostHanaTests::testHandlingTypeMismatch() +{ + JSONParseErrors errors; + NestingArrayHana::fromJson("{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793," + "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false},{\"number\":43,\"number2\":3." + "141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}]}", + &errors); + CPPUNIT_ASSERT_EQUAL(0_st, errors.size()); + + NestingObjectHana::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":\"42\",\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":" + "\"test\",\"boolean\":false}}", + &errors); + CPPUNIT_ASSERT_EQUAL(1_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.front().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors.front().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors.front().actualType); + CPPUNIT_ASSERT_EQUAL("number"s, string(errors.front().member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors.front().record)); + errors.clear(); + + NestingObjectHana::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":1,\"text\":" + "\"test\",\"boolean\":false}}", + &errors); + CPPUNIT_ASSERT_EQUAL(1_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.front().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Array, errors.front().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors.front().actualType); + CPPUNIT_ASSERT_EQUAL("numbers"s, string(errors.front().member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors.front().record)); + errors.clear(); + + NestingObjectHana::fromJson("{\"name\":[],\"testObj\":\"this is not an object\"}", &errors); + CPPUNIT_ASSERT_EQUAL(2_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.front().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors.front().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::Array, errors.front().actualType); + CPPUNIT_ASSERT_EQUAL("name"s, string(errors.front().member)); + CPPUNIT_ASSERT_EQUAL("NestingObject"s, string(errors.front().record)); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.back().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Object, errors.back().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors.back().actualType); + CPPUNIT_ASSERT_EQUAL("testObj"s, string(errors.back().member)); + CPPUNIT_ASSERT_EQUAL("NestingObject"s, string(errors.back().record)); + errors.clear(); + + const NestingArrayHana nestingArray( + NestingArrayHana::fromJson("{\"name\":\"nesting2\",\"testObjects\":[25,{\"number\":42,\"number2\":3.141592653589793," + "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false},\"foo\",{\"number\":43,\"number2\":3." + "141592653589793,\"numbers\":[1,2,3,4,\"bar\"],\"text\":\"test\",\"boolean\":false}]}", + &errors)); + CPPUNIT_ASSERT_EQUAL(3_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors[0].kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Object, errors[0].expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors[0].actualType); + CPPUNIT_ASSERT_EQUAL("testObjects"s, string(errors[0].member)); + CPPUNIT_ASSERT_EQUAL("NestingArray"s, string(errors[0].record)); + CPPUNIT_ASSERT_EQUAL(0_st, errors[0].index); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors[1].kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Object, errors[1].expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors[1].actualType); + CPPUNIT_ASSERT_EQUAL(2_st, errors[1].index); + CPPUNIT_ASSERT_EQUAL("testObjects"s, string(errors[1].member)); + CPPUNIT_ASSERT_EQUAL("NestingArray"s, string(errors[1].record)); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors[2].kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors[2].expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors[2].actualType); + CPPUNIT_ASSERT_EQUAL("numbers"s, string(errors[2].member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors[2].record)); + CPPUNIT_ASSERT_EQUAL(4_st, errors[2].index); + errors.clear(); + + errors.throwOn = JSONParseErrors::ThrowOn::TypeMismatch; + CPPUNIT_ASSERT_THROW(NestingObjectHana::fromJson("{\"name\":[],\"testObj\":\"this is not an object\"}", &errors), JSONParseError); +} diff --git a/lib/tests/jsonreflector.cpp b/lib/tests/jsonreflector.cpp index 855843f..48abebe 100644 --- a/lib/tests/jsonreflector.cpp +++ b/lib/tests/jsonreflector.cpp @@ -73,28 +73,54 @@ template <> inline void push(const NestingArray &reflectable, Valu push(reflectable.testObjects, "testObjects", value, allocator); } -template <> inline void pull(TestObject &reflectable, const GenericValue>::ConstObject &value) +template <> inline void pull(TestObject &reflectable, const GenericValue>::ConstObject &value, JSONParseErrors *errors) { - pull(reflectable.number, "number", value); - pull(reflectable.number2, "number2", value); - pull(reflectable.numbers, "numbers", value); - pull(reflectable.text, "text", value); - pull(reflectable.boolean, "boolean", value); + const char *previousRecord; + if (errors) { + previousRecord = errors->currentRecord; + errors->currentRecord = "TestObject"; + } + pull(reflectable.number, "number", value, errors); + pull(reflectable.number2, "number2", value, errors); + pull(reflectable.numbers, "numbers", value, errors); + pull(reflectable.text, "text", value, errors); + pull(reflectable.boolean, "boolean", value, errors); + if (errors) { + errors->currentRecord = previousRecord; + } } -template <> inline void pull(NestingObject &reflectable, const GenericValue>::ConstObject &value) +template <> inline void pull(NestingObject &reflectable, const GenericValue>::ConstObject &value, JSONParseErrors *errors) { - pull(reflectable.name, "name", value); - pull(reflectable.testObj, "testObj", value); + const char *previousRecord; + if (errors) { + previousRecord = errors->currentRecord; + errors->currentRecord = "NestingObject"; + } + pull(reflectable.name, "name", value, errors); + pull(reflectable.testObj, "testObj", value, errors); + if (errors) { + errors->currentRecord = previousRecord; + } } -template <> inline void pull(NestingArray &reflectable, const GenericValue>::ConstObject &value) +template <> inline void pull(NestingArray &reflectable, const GenericValue>::ConstObject &value, JSONParseErrors *errors) { - pull(reflectable.name, "name", value); - pull(reflectable.testObjects, "testObjects", value); + const char *previousRecord; + if (errors) { + previousRecord = errors->currentRecord; + errors->currentRecord = "NestingArray"; + } + pull(reflectable.name, "name", value, errors); + pull(reflectable.testObjects, "testObjects", value, errors); + if (errors) { + errors->currentRecord = previousRecord; + } } } // namespace Reflector + +// namespace Reflector } // namespace ReflectiveRapidJSON /// \endcond @@ -257,13 +283,14 @@ void JSONReflectorTests::testDeserializePrimitives() bool bool1 = false, bool2 = true; float float1 = 0.0; double double1 = 0.0; - Reflector::pull(str1, array); - Reflector::pull(int1, array); - Reflector::pull(float1, array); - Reflector::pull(str2, array); - Reflector::pull(bool1, array); - Reflector::pull(double1, array); - Reflector::pull(bool2, array); + JSONParseErrors errors; + Reflector::pull(str1, array, &errors); + Reflector::pull(int1, array, &errors); + Reflector::pull(float1, array, &errors); + Reflector::pull(str2, array, &errors); + Reflector::pull(bool1, array, &errors); + Reflector::pull(double1, array, &errors); + Reflector::pull(bool2, array, &errors); CPPUNIT_ASSERT_EQUAL("a"s, str1); CPPUNIT_ASSERT_EQUAL(5, int1); @@ -320,6 +347,9 @@ void JSONReflectorTests::testDeserializeNestedObjects() } } +/*! + * \brief Tests whether RAPIDJSON_NAMESPACE::ParseResult is thrown correctly when passing invalid JSON to fromJSON(). + */ void JSONReflectorTests::testHandlingParseError() { try { @@ -332,13 +362,80 @@ void JSONReflectorTests::testHandlingParseError() } } +/*! + * \brief Tests whether JSONParseError is thrown on type mismatch. + */ void JSONReflectorTests::testHandlingTypeMismatch() { + JSONParseErrors errors; + NestingArray::fromJson("{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793," + "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false},{\"number\":43,\"number2\":3." + "141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}]}", + &errors); + CPPUNIT_ASSERT_EQUAL(0_st, errors.size()); + NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":\"42\",\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":" - "\"test\",\"boolean\":false}}"); + "\"test\",\"boolean\":false}}", + &errors); + CPPUNIT_ASSERT_EQUAL(1_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.front().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors.front().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors.front().actualType); + CPPUNIT_ASSERT_EQUAL("number"s, string(errors.front().member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors.front().record)); + errors.clear(); + NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":1,\"text\":" - "\"test\",\"boolean\":false}}"); - NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":\"this is not an object\"}"); - NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":\"42\",\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":" - "\"test\",\"boolean\":\"false\"}}"); + "\"test\",\"boolean\":false}}", + &errors); + CPPUNIT_ASSERT_EQUAL(1_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.front().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Array, errors.front().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors.front().actualType); + CPPUNIT_ASSERT_EQUAL("numbers"s, string(errors.front().member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors.front().record)); + errors.clear(); + + NestingObject::fromJson("{\"name\":[],\"testObj\":\"this is not an object\"}", &errors); + CPPUNIT_ASSERT_EQUAL(2_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.front().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors.front().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::Array, errors.front().actualType); + CPPUNIT_ASSERT_EQUAL("name"s, string(errors.front().member)); + CPPUNIT_ASSERT_EQUAL("NestingObject"s, string(errors.front().record)); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors.back().kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Object, errors.back().expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors.back().actualType); + CPPUNIT_ASSERT_EQUAL("testObj"s, string(errors.back().member)); + CPPUNIT_ASSERT_EQUAL("NestingObject"s, string(errors.back().record)); + errors.clear(); + + const NestingArray nestingArray( + NestingArray::fromJson("{\"name\":\"nesting2\",\"testObjects\":[25,{\"number\":42,\"number2\":3.141592653589793," + "\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false},\"foo\",{\"number\":43,\"number2\":3." + "141592653589793,\"numbers\":[1,2,3,4,\"bar\"],\"text\":\"test\",\"boolean\":false}]}", + &errors)); + CPPUNIT_ASSERT_EQUAL(3_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors[0].kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Object, errors[0].expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors[0].actualType); + CPPUNIT_ASSERT_EQUAL("testObjects"s, string(errors[0].member)); + CPPUNIT_ASSERT_EQUAL("NestingArray"s, string(errors[0].record)); + CPPUNIT_ASSERT_EQUAL(0_st, errors[0].index); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors[1].kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Object, errors[1].expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors[1].actualType); + CPPUNIT_ASSERT_EQUAL(2_st, errors[1].index); + CPPUNIT_ASSERT_EQUAL("testObjects"s, string(errors[1].member)); + CPPUNIT_ASSERT_EQUAL("NestingArray"s, string(errors[1].record)); + CPPUNIT_ASSERT_EQUAL(JSONParseErrorKind::TypeMismatch, errors[2].kind); + CPPUNIT_ASSERT_EQUAL(JSONType::Number, errors[2].expectedType); + CPPUNIT_ASSERT_EQUAL(JSONType::String, errors[2].actualType); + CPPUNIT_ASSERT_EQUAL("numbers"s, string(errors[2].member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors[2].record)); + CPPUNIT_ASSERT_EQUAL(4_st, errors[2].index); + errors.clear(); + + errors.throwOn = JSONParseErrors::ThrowOn::TypeMismatch; + CPPUNIT_ASSERT_THROW(NestingObject::fromJson("{\"name\":[],\"testObj\":\"this is not an object\"}", &errors), JSONParseError); }