From 299b395febe41c36af62095e33739b243f3c4093 Mon Sep 17 00:00:00 2001 From: Christian Heimlich Date: Fri, 11 Aug 2023 16:53:50 -0400 Subject: [PATCH] Add context to JsonError JSON parsing routines now track the "JSON node stack" as context, so a valid JsonError's details will contain the position within the JSON tree where the error occurred. --- lib/core/doc/res/snippets/qx-json.cpp | 18 ++- lib/core/include/qx/core/qx-json.h | 138 +++++++++++++---- lib/core/src/qx-json.cpp | 210 +++++++++++++++++++++++--- 3 files changed, 313 insertions(+), 53 deletions(-) diff --git a/lib/core/doc/res/snippets/qx-json.cpp b/lib/core/doc/res/snippets/qx-json.cpp index e8df25ae..fb8ddd81 100644 --- a/lib/core/doc/res/snippets/qx-json.cpp +++ b/lib/core/doc/res/snippets/qx-json.cpp @@ -107,6 +107,18 @@ QX_JSON_MEMBER_OVERRIDE(MySpecialStruct, name, //! [5] //! [6] +QJsonArray ja; +// Somehow populate array... + +for(int i = 0; i < ja.size(); ++i) +{ + // Parse element + if(Qx::JsonError je = parseMyElement(ja[i]); je.isValid()) + return je.withContext(QxJson::Array()).withContext(QxJson::ArrayElement(i)); +} +//! [6] + +//! [7] class MyType { ... @@ -127,9 +139,9 @@ namespace QxJson } }; } -//! [6] - //! [7] + +//! [8] struct MyStruct { int number; @@ -158,4 +170,4 @@ struct OtherStruct QX_JSON_STRUCT(enabled, myStructs); }; -//! [7] \ No newline at end of file +//! [8] \ No newline at end of file diff --git a/lib/core/include/qx/core/qx-json.h b/lib/core/include/qx/core/qx-json.h index 3d3ad239..5df17df9 100644 --- a/lib/core/include/qx/core/qx-json.h +++ b/lib/core/include/qx/core/qx-json.h @@ -10,6 +10,8 @@ #include #include #include +#include +#include // Intra-component Includes #include "qx/core/qx-abstracterror.h" @@ -76,6 +78,75 @@ namespace QxJson \ }; \ } +namespace QxJson +{ + +class File +{ +private: + QString mIdentifier; + +public: + File(const QString& filename); + File(const QFile& docFile); + File(const QFileInfo& docFile); + + QString string() const; +}; + +class Document +{ +private: + QString mName; + +public: + Document(const QString& name = {}); + + QString string() const; +}; + +class Object +{ +public: + Object(); + + QString string() const; +}; + +class ObjectKey +{ +private: + QString mName; + +public: + ObjectKey(const QString& name); + + QString string() const; +}; + +class Array +{ +public: + Array(); + + QString string() const; +}; + +class ArrayElement +{ +private: + uint mElement; + +public: + ArrayElement(uint element); + + QString string() const; +}; + +using ContextNode = std::variant; + +} // namespace QxJson + namespace Qx { @@ -106,22 +177,26 @@ class QX_CORE_EXPORT JsonError final : public AbstractError<"Qx::JsonError", 5> private: QString mAction; Form mForm; + QList mContext; -//-Class Constructor------------------------------------------------------------- +//-Constructor----------------------------------------------------------------- public: JsonError(); JsonError(const QString& a, Form f); -//-Class Functions------------------------------------------------------------- -public: - bool isValid() const; - QString action() const; - Form form() const; - +//-Instance Functions------------------------------------------------------------- private: quint32 deriveValue() const override; QString derivePrimary() const override; QString deriveSecondary() const override; + QString deriveDetails() const override; + +public: + bool isValid() const; + QString action() const; + Form form() const; + QList context() const; + JsonError& withContext(const QxJson::ContextNode& node); }; } // namespace Qx @@ -315,7 +390,8 @@ struct Converter static Qx::JsonError fromJson(T& value, const QJsonValue& jValue) { if(!jValue.isObject()) - return Qx::JsonError(QxJsonPrivate::ERR_CONV_TYPE.arg(QxJsonPrivate::typeString()), Qx::JsonError::TypeMismatch); + return Qx::JsonError(QxJsonPrivate::ERR_CONV_TYPE.arg(QxJsonPrivate::typeString()), Qx::JsonError::TypeMismatch) + .withContext(QxJson::Object()); // Underlying object QJsonObject jObject = jValue.toObject(); @@ -342,7 +418,8 @@ struct Converter // Get value from key if(!jObject.contains(mKey)) { - cnvError = Qx::JsonError(QxJsonPrivate::ERR_NO_KEY.arg(mKey), Qx::JsonError::MissingKey); + cnvError = Qx::JsonError(QxJsonPrivate::ERR_NO_KEY.arg(mKey), Qx::JsonError::MissingKey) + .withContext(QxJson::Object()); return false; } QJsonValue mValue = jObject.value(mKey); @@ -353,6 +430,7 @@ struct Converter else cnvError = QxJsonPrivate::performRegularConversion(value.*mPtr, mValue); + cnvError.withContext(QxJson::Object()).withContext(QxJson::ObjectKey(mKey)); return !cnvError.isValid(); }() && ...); }, memberMetas); @@ -373,7 +451,10 @@ struct Converter value.clear(); if(!jValue.isArray()) - return Qx::JsonError(QxJsonPrivate::ERR_CONV_TYPE.arg(QxJsonPrivate::typeString()), Qx::JsonError::TypeMismatch); + { + return Qx::JsonError(QxJsonPrivate::ERR_CONV_TYPE.arg(QxJsonPrivate::typeString()), Qx::JsonError::TypeMismatch) + .withContext(QxJson::Array()); + } // Underlying Array QJsonArray jArray = jValue.toArray(); @@ -382,13 +463,13 @@ struct Converter Qx::JsonError cnvError; // Convert all - for(const QJsonValue& aValue : jArray) + for(auto i = 0; i < jArray.count(); ++i) { E converted; - if(cnvError = Converter::fromJson(converted, aValue); cnvError.isValid()) + if(cnvError = Converter::fromJson(converted, jArray[i]); cnvError.isValid()) { value.clear(); - return cnvError; + return cnvError.withContext(QxJson::Array()).withContext(QxJson::ArrayElement(i)); } value << converted; @@ -411,8 +492,10 @@ struct Converter value.clear(); if(!jValue.isArray()) - return Qx::JsonError(QxJsonPrivate::ERR_CONV_TYPE.arg(QxJsonPrivate::typeString()), Qx::JsonError::TypeMismatch); - + { + return Qx::JsonError(QxJsonPrivate::ERR_CONV_TYPE.arg(QxJsonPrivate::typeString()), Qx::JsonError::TypeMismatch) + .withContext(QxJson::Array()); + } // Underlying Array QJsonArray jArray = jValue.toArray(); @@ -420,13 +503,13 @@ struct Converter Qx::JsonError cnvError; // Convert all - for(const QJsonValue& aValue : jArray) + for(auto i = 0; i < jArray.count(); ++i) { V converted; - if(cnvError = Converter::fromJson(converted, aValue); cnvError.isValid()) + if(cnvError = Converter::fromJson(converted, jArray[i]); cnvError.isValid()) { value.clear(); - return cnvError; + return cnvError.withContext(QxJson::Array()).withContext(QxJson::ArrayElement(i)); } value.insert(keygen(converted), converted); @@ -491,31 +574,22 @@ template JsonError parseJson(T& parsed, const QJsonDocument& doc) { if(doc.isEmpty()) - return JsonError(QxJsonPrivate::ERR_PARSE_DOC, JsonError::EmptyDoc); + return JsonError(QxJsonPrivate::ERR_PARSE_DOC, JsonError::EmptyDoc).withContext(QxJson::Document()); - using RootType = std::conditional_t< - (QxJson::json_containing), QJsonArray, QJsonObject>; - - RootType root; if constexpr(QxJson::json_containing) { if(!doc.isArray()) - return JsonError(QxJsonPrivate::ERR_PARSE_DOC, JsonError::TypeMismatch); + return JsonError(QxJsonPrivate::ERR_PARSE_DOC, JsonError::TypeMismatch).withContext(QxJson::Document()); - root = doc.array(); + return parseJson(parsed, doc.array()).withContext(QxJson::Document()); } else { if(!doc.isObject()) - return JsonError(QxJsonPrivate::ERR_PARSE_DOC, JsonError::TypeMismatch); + return JsonError(QxJsonPrivate::ERR_PARSE_DOC, JsonError::TypeMismatch).withContext(QxJson::Document()); - root = doc.object(); + return parseJson(parsed, doc.object()).withContext(QxJson::Document()); } - - // Use QJsonValue move constructor for semi-type erasure - QJsonValue rootAsValue = std::move(root); - - return QxJson::Converter::fromJson(parsed, rootAsValue); } template diff --git a/lib/core/src/qx-json.cpp b/lib/core/src/qx-json.cpp index 2c163aeb..d21dc5ea 100644 --- a/lib/core/src/qx-json.cpp +++ b/lib/core/src/qx-json.cpp @@ -109,7 +109,7 @@ namespace Qx // JsonError //=============================================================================================================== -// Enum +//-Class Enums------------------------------------------------------------- /*! * @class JsonError qx/core/qx-json.h * @@ -120,19 +120,15 @@ namespace Qx * @enum JsonError::Form * * This enum represents form of JSON error. - */ - -/*! + * * @var JsonError::Form JsonError::NoError * No error occurred. - */ - -/*! + * * @var JsonError::Form JsonError::MissingKey * An expected key was missing. */ -// Ctor +//-Constructor----------------------------------------------------------------- /*! * Creates an invalid JsonError. */ @@ -149,7 +145,23 @@ JsonError::JsonError(const QString& a, Form f) : mForm(f) {} -// Functions +//-Instance Functions------------------------------------------------------------- +//Private: +quint32 JsonError::deriveValue() const { return mForm; }; +QString JsonError::derivePrimary() const { return mAction; }; +QString JsonError::deriveSecondary() const { return ERR_STRINGS.value(mForm); }; + +QString JsonError::deriveDetails() const +{ + QString details = u"JSON Node Stack:\n"_s; + for(const auto& node : mContext) + details += u"- "_s + std::visit([](auto&& n){ return n.string(); }, node) + u"\n"_s; + details += u"\nAction: "_s + derivePrimary() + u"\n\n"_s + u"Error: "_s + deriveSecondary(); + + return details; +} + +//Public: /*! * Returns @c true if an error occurred; otherwise, returns @c false. */ @@ -165,10 +177,32 @@ QString JsonError::action() const { return mAction; } */ JsonError::Form JsonError::form() const { return mForm; } -// Functions -quint32 JsonError::deriveValue() const { return mForm; }; -QString JsonError::derivePrimary() const { return mAction; }; -QString JsonError::deriveSecondary() const { return ERR_STRINGS.value(mForm); }; +/*! + * Provides the full context in which the error occurred. + */ +QList JsonError::context() const { return mContext; } + +/*! + * Prepends the context node @a node to the error's context and returns a reference + * to the updated error object. This function is a no-op if the error is not valid. + * + * Generally this function is of no use in user code, but can be utilized when + * providing complex custom parsing for user types via QX_JSON_MEMBER_OVERRIDE + * or Converter specializations. + * + * Typically, this function is used in a chain of return calls in order to pass + * the context of where an error occurred up the call stack. For example, one might + * return information about a JSON array related error like so: + * + * @snippet qx-json.cpp 6 + */ +JsonError& JsonError::withContext(const QxJson::ContextNode& node) +{ + if(!isValid()) + return *this; + + mContext.prepend(node); return *this; +} //=============================================================================================================== // QJsonParseErrorAdapter @@ -290,9 +324,6 @@ QString asString(const QJsonValue& value) namespace QxJson { -//=============================================================================================================== -// -//=============================================================================================================== /*! * @namespace QxJson @@ -300,6 +331,149 @@ namespace QxJson * @brief The @c QxJson namespace encapsulates the user-extensible implementation of Qx's JSON parsing facilities. */ +//=============================================================================================================== +// Nodes +//=============================================================================================================== + +/*! + * @class File + * @brief The file class represents a JSON file node for use in error contexts. + * + * @note This class is irrelevant in user code except for some instances of complex + * custom JSON parsing + * + * @sa Qx::JsonError::withContext(). + */ + +/*! + * Constructs a file node with the identifier of @a filename. + */ +File::File(const QString& filename) : mIdentifier(filename) {} + +/*! + * Constructs a file node with the identifier set to the filename of @a docFile. + */ +File::File(const QFile& docFile) : mIdentifier(docFile.fileName()) {} + +/*! + * Constructs a file node with the identifier set to the absolute path of @a docFile. + */ +File::File(const QFileInfo& docFile) : mIdentifier(docFile.absoluteFilePath()) {} + +/*! + * Returns the string representation of the node. + */ +QString File::string() const { return u"File: "_s + mIdentifier; } + +/*! + * @class Document + * @brief The document class represents a JSON document node for use in error contexts. + * + * @note This class is irrelevant in user code except for some instances of complex + * custom JSON parsing + * + * @sa Qx::JsonError::withContext(). + */ + +/*! + * Constructs a document element node with the name @a name, which defaults to no name. + */ +Document::Document(const QString& name) : mName(name) {} + +/*! + * Returns the string representation of the node. + */ +QString Document::string() const { return u"Document"_s + (!mName.isNull() ? u": "_s + mName : QString()); } + +/*! + * @class Object + * @brief The object class represents a JSON object node for use in error contexts. + * + * @note This class is irrelevant in user code except for some instances of complex + * custom JSON parsing + * + * @sa Qx::JsonError::withContext(). + */ + +/*! + * Constructs an object element node. + */ +Object::Object() {} + +/*! + * Returns the string representation of the node. + */ +QString Object::string() const { return u"Object"_s; } + +/*! + * @class ObjectKey + * @brief The object key class represents a JSON Object key node for use in error contexts. + * + * @note This class is irrelevant in user code except for some instances of complex + * custom JSON parsing + * + * @sa Qx::JsonError::withContext(). + */ + +/*! + * Constructs an object key node with the key name @a name. + */ +ObjectKey::ObjectKey(const QString& name) : mName(name) {} + +/*! + * Returns the string representation of the node. + */ +QString ObjectKey::string() const { return u"Key: "_s + mName; } + +/*! + * @class Array + * @brief The array class represents a JSON array node for use in error contexts. + * + * @note This class is irrelevant in user code except for some instances of complex + * custom JSON parsing + * + * @sa Qx::JsonError::withContext(). + */ + +/*! + * Constructs an array element node. + */ +Array::Array() {} + +/*! + * Returns the string representation of the node. + */ +QString Array::string() const { return u"Array"_s; } + +/*! + * @class ArrayElement + * @brief The array element key class represents a JSON Array element node for use in error contexts. + * + * @note This class is irrelevant in user code except for some instances of complex + * custom JSON parsing + * + * @sa Qx::JsonError::withContext(). + */ + +/*! + * Constructs an array element node with the element number @a element. + */ +ArrayElement::ArrayElement(uint element) : mElement(element) {} + +/*! + * Returns the string representation of the node. + */ +QString ArrayElement::string() const { return (u"Element: %1"_s).arg(mElement); } + +/*! + * @typedef ContextNode + * Any JSON context node type. + */ + +//=============================================================================================================== +// +//=============================================================================================================== + /*! * @struct Converter * @@ -321,7 +495,7 @@ namespace QxJson * - QMap (when a keygen() specialization exists for K) * * Support for additional, non-structural types can be added like so: - * @snippet qx-json.cpp 6 + * @snippet qx-json.cpp 7 * * If a structural type needs to be registered, the QX_JSON_STRUCT and QX_JSON_MEMBER macros should be used * instead. @@ -339,7 +513,7 @@ namespace QxJson * for that type. * * Support for additional types can be added like so: - * @snippet qx-json.cpp 7 + * @snippet qx-json.cpp 8 * * @sa qx-json.h and Converter. */