diff --git a/CMakeLists.txt b/CMakeLists.txt index 6408edb4..8158c348 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,21 +1,21 @@ #================= Project Setup ========================== # CMake -cmake_minimum_required(VERSION 3.23.0...3.26.0) +cmake_minimum_required(VERSION 3.23.0...3.31.0) # Project # NOTE: For versions stick to w.x.y.z, where z is generally # avoided and only used for hotfixes. DON'T USE TRAILING # ZEROS IN VERSIONS project(Qx - VERSION 0.6.2 + VERSION 0.6.3 LANGUAGES CXX DESCRIPTION "Qt Extensions Library" ) # Get helper scripts include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake) -fetch_ob_cmake("v0.3.9") +fetch_ob_cmake("v0.3.10") # Initialize project according to standard rules include(OB/Project) diff --git a/lib/core/include/qx/core/qx-bitarray.h b/lib/core/include/qx/core/qx-bitarray.h index 07395084..c5265af1 100644 --- a/lib/core/include/qx/core/qx-bitarray.h +++ b/lib/core/include/qx/core/qx-bitarray.h @@ -14,8 +14,8 @@ class QX_CORE_EXPORT BitArray : public QBitArray { //-Constructor-------------------------------------------------------------------------------------------------- public: - BitArray(); - BitArray(int size, bool value = false); + BitArray() noexcept; + explicit BitArray(int size, bool value = false); //-Class Functions---------------------------------------------------------------------------------------------- public: @@ -38,7 +38,7 @@ class QX_CORE_EXPORT BitArray : public QBitArray public: template requires std::integral - T toInteger() + T toInteger() const { int bitCount = sizeof(T)*8; T integer = 0; @@ -49,7 +49,7 @@ class QX_CORE_EXPORT BitArray : public QBitArray return integer; } - QByteArray toByteArray(QSysInfo::Endian endianness = QSysInfo::BigEndian); + QByteArray toByteArray(QSysInfo::Endian endianness = QSysInfo::BigEndian) const; void append(bool bit = false); void replace(const BitArray& bits, int start = 0, int length = -1); @@ -62,15 +62,15 @@ class QX_CORE_EXPORT BitArray : public QBitArray replace(converted, start, length); } - BitArray subArray(int start, int length = -1); + BitArray subArray(int start, int length = -1) const; BitArray takeFromStart(int length = -1); BitArray takeFromEnd(int length = -1); - BitArray operator<<(int n); + BitArray operator<<(int n) const; void operator<<=(int n); - BitArray operator>>(int n); + BitArray operator>>(int n) const; void operator>>=(int n); - BitArray operator+(BitArray rhs); + BitArray operator+(BitArray rhs) const; void operator+=(const BitArray& rhs); }; diff --git a/lib/core/include/qx/core/qx-json.h b/lib/core/include/qx/core/qx-json.h index aa2ee475..424cdb8f 100644 --- a/lib/core/include/qx/core/qx-json.h +++ b/lib/core/include/qx/core/qx-json.h @@ -96,6 +96,14 @@ class QX_CORE_EXPORT File QString string() const; }; +class QX_CORE_EXPORT Data +{ +public: + Data(); + + QString string() const; +}; + class QX_CORE_EXPORT Document { private: @@ -145,7 +153,7 @@ class QX_CORE_EXPORT ArrayElement QString string() const; }; -using ContextNode = std::variant; +using ContextNode = std::variant; } // namespace QxJson @@ -163,6 +171,7 @@ class QX_CORE_EXPORT JsonError final : public AbstractError<"Qx::JsonError", 5> TypeMismatch, EmptyDoc, InvalidValue, + InvalidData, MissingFile, InaccessibleFile, FileReadError, @@ -177,6 +186,7 @@ class QX_CORE_EXPORT JsonError final : public AbstractError<"Qx::JsonError", 5> {TypeMismatch, u"Value type mismatch."_s}, {EmptyDoc, u"The document is empty."_s}, {InvalidValue, u"Invalid value for type."_s}, + {InvalidData, u"Data parse error."_s}, {MissingFile, u"File does not exist."_s}, {InaccessibleFile, u"Cannot open the file."_s}, {FileReadError, u"File read error."_s}, @@ -289,6 +299,7 @@ static inline const QString ERR_CONV_TYPE = u"JSON Error: Converting value to %1 static inline const QString ERR_NO_KEY = u"JSON Error: Could not retrieve key '%1'."_s; static inline const QString ERR_PARSE_DOC = u"JSON Error: Could not parse JSON document."_s; static inline const QString ERR_READ_FILE = u"JSON Error: Could not read JSON file."_s; +static inline const QString ERR_READ_DATA = u"JSON Error: Could not read JSON data."_s; static inline const QString ERR_WRITE_FILE = u"JSON Error: Could not write JSON file."_s; //-Structs--------------------------------------------------------------- @@ -788,10 +799,45 @@ void serializeJson(QJsonDocument& serialized, const T& root) serialized = QJsonDocument(QxJson::Converter::toJson(root)); } +template + requires json_root +JsonError parseJson(T& parsed, const QByteArray& data) +{ + // Check for no data + if(data.isEmpty()) + return JsonError(QxJsonPrivate::ERR_READ_DATA, JsonError::EmptyDoc).withContext(QxJson::Data()); + + // Basic parse + QJsonParseError jpe; + QJsonDocument jd = QJsonDocument::fromJson(data, &jpe); + + if(jpe.error != jpe.NoError) + return JsonError(QxJsonPrivate::ERR_READ_DATA, JsonError::InvalidData).withContext(QxJson::Data()); + + // True parse + return parseJson(parsed, jd).withContext(QxJson::Data()); +} + +template + requires json_root +void serializeJson(QByteArray& serialized, const T& root, QJsonDocument::JsonFormat fmt = QJsonDocument::Indented) +{ + // Ensure buffer is clear + serialized.clear(); + + // Create document + QJsonDocument jd; + serializeJson(jd, root); + + // Write data + serialized = jd.toJson(fmt); +} + template requires json_root JsonError parseJson(T& parsed, QFile& file) { + // NOTE: Don't utilize the QByteArray "data" overload here as we would lose the better error info if(!file.exists()) return JsonError(QxJsonPrivate::ERR_READ_FILE, JsonError::MissingFile).withContext(QxJson::File(file.fileName())); @@ -828,7 +874,7 @@ JsonError parseJson(T& parsed, QFile& file) template requires json_root -JsonError serializeJson(QFile& serialized, const T& root) +JsonError serializeJson(QFile& serialized, const T& root, QJsonDocument::JsonFormat fmt = QJsonDocument::Indented) { // Close and re-open file, if open, to ensure correct mode and start of file if(serialized.isOpen()) @@ -840,15 +886,9 @@ JsonError serializeJson(QFile& serialized, const T& root) // Close file when finished QScopeGuard fileGuard([&serialized]{ serialized.close(); }); - // Create document - QJsonDocument jd; - serializeJson(jd, root); - - // Write data - QByteArray jsonData = jd.toJson(); - if(jsonData.isEmpty()) - return JsonError(); // No-op - + // Serialize + QByteArray jsonData; + serializeJson(jsonData, root, fmt); if(serialized.write(jsonData) != jsonData.size()) return JsonError(QxJsonPrivate::ERR_WRITE_FILE, JsonError::FileWriteError).withContext(QxJson::File(serialized.fileName(), serialized.errorString())); @@ -866,11 +906,11 @@ JsonError parseJson(T& parsed, const QString& filePath) template requires json_root -JsonError serializeJson(const QString& serializedPath, const T& root) +JsonError serializeJson(const QString& serializedPath, const T& root, QJsonDocument::JsonFormat fmt = QJsonDocument::Indented) { QFile file(serializedPath); - return serializeJson(file, root); + return serializeJson(file, root, fmt); } QX_CORE_EXPORT QList findAllValues(const QJsonValue& rootValue, QStringView key); diff --git a/lib/core/include/qx/core/qx-string.h b/lib/core/include/qx/core/qx-string.h index 5393b4b4..760e1f79 100644 --- a/lib/core/include/qx/core/qx-string.h +++ b/lib/core/include/qx/core/qx-string.h @@ -66,6 +66,8 @@ class QX_CORE_EXPORT String static QString trimTrailing(const QStringView string); static QString mapArg(QAnyStringView s, const QMap& map, Qt::CaseSensitivity cs = Qt::CaseSensitive); + + static QString toHeadlineCase(const QString& string); }; } diff --git a/lib/core/src/qx-bitarray.cpp b/lib/core/src/qx-bitarray.cpp index a1181f66..8914afa1 100644 --- a/lib/core/src/qx-bitarray.cpp +++ b/lib/core/src/qx-bitarray.cpp @@ -1,9 +1,6 @@ // Unit Includes #include "qx/core/qx-bitarray.h" -// Standard Library Includes -#include - namespace Qx { //=============================================================================================================== @@ -39,7 +36,7 @@ namespace Qx /*! * Constructs an empty bit array. */ -BitArray::BitArray() : QBitArray() {} +BitArray::BitArray() noexcept : QBitArray() {} /*! * Constructs a bit array containing @a size bits. The bits are @@ -75,7 +72,7 @@ BitArray::BitArray(int size, bool value) : QBitArray(size, value) {} * @warning The accuracy of the provided @a endianness relies on the contents of the bit array being in * big-endian order, since using QSysInfo::LittleEndian simply reverses the resultant byte array's byte order. */ -QByteArray BitArray::toByteArray(QSysInfo::Endian endianness) +QByteArray BitArray::toByteArray(QSysInfo::Endian endianness) const { // Byte array QByteArray ba(std::ceil(count()/8.0), 0); @@ -134,7 +131,7 @@ void BitArray::replace(const BitArray& bits, int start, int length) * * A value of -1 for @a length will result all bits from @a start to the end of the array being included. */ -BitArray BitArray::subArray(int start, int length) +BitArray BitArray::subArray(int start, int length) const { if(start < 0 || start >= count()) qFatal("Least significant bit index was outside BitArray contents"); @@ -196,7 +193,7 @@ BitArray BitArray::takeFromEnd(int length) /*! * Returns a new bit array with the contents of the original shifted left @a n times. */ -BitArray BitArray::operator<<(int n) +BitArray BitArray::operator<<(int n) const { BitArray shifted(count()); @@ -220,7 +217,7 @@ void BitArray::operator<<=(int n) /*! * Returns a new bit array with the contents of the original shifted right @a n times. */ -BitArray BitArray::operator>>(int n) +BitArray BitArray::operator>>(int n) const { BitArray shifted(count()); @@ -244,7 +241,7 @@ void BitArray::operator>>=(int n) /*! * Returns a bit array which is the result of concatenating this bit array and @a rhs. */ -BitArray BitArray::operator+(BitArray rhs) +BitArray BitArray::operator+(BitArray rhs) const { BitArray sum(count() + rhs.count()); sum |= *this; diff --git a/lib/core/src/qx-json.cpp b/lib/core/src/qx-json.cpp index 5cfd9080..d448b1c8 100644 --- a/lib/core/src/qx-json.cpp +++ b/lib/core/src/qx-json.cpp @@ -324,6 +324,28 @@ QString QJsonParseErrorAdapter::deriveSecondary() const { return OFFSET_STR.arg( * @a T must satisfy the Qx::json_root concept. */ +/*! + * @fn JsonError parseJson(T& parsed, const QByteArray& data) + * + * @overload + * + * Parses @a data as a JSON document and stores the result in @a parsed. + * @a T must satisfy the Qx::json_root concept. + */ + +/*! + * @fn JsonError serializeJson(QByteArray& serialized, const T& root, QJsonDocument::JsonFormat fmt) + * + * @overload + * + * Serializes the entire JSON root structure @a root and writes the result to @a serialized in format @a fmt. + * + * @a T must satisfy the Qx::json_root concept. + * + * If serialization fails, a valid JsonError is returned that describes the cause; otherwise, an invalid + * error is returned. + */ + /*! * @fn JsonError parseJson(T& parsed, QFile& file) * @@ -334,11 +356,11 @@ QString QJsonParseErrorAdapter::deriveSecondary() const { return OFFSET_STR.arg( */ /*! - * @fn JsonError serializeJson(QFile& serialized, const T& root) + * @fn JsonError serializeJson(QFile& serialized, const T& root, QJsonDocument::JsonFormat fmt) * * @overload * - * Serializes the entire JSON root structure @a root and writes the result to @a serialized in indented format, + * Serializes the entire JSON root structure @a root and writes the result to @a serialized in format @a fmt, * replacing existing contents, if any. * * @a T must satisfy the Qx::json_root concept. @@ -357,12 +379,12 @@ QString QJsonParseErrorAdapter::deriveSecondary() const { return OFFSET_STR.arg( */ /*! - * @fn JsonError serializeJson(const QString& filePath, const T& root) + * @fn JsonError serializeJson(const QString& filePath, const T& root, QJsonDocument::JsonFormat fmt) * * @overload * * Serializes the entire JSON root structure @a root and writes the result to the file at path @a - * filePath in indented format, replacing existing contents, if any. + * filePath in format @a fmt, replacing existing contents, if any. * * @a T must satisfy the Qx::json_root concept. * @@ -476,6 +498,26 @@ QString File::string() const return str; } +/*! + * @class Data + * @brief The document class represents a JSON data 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 data element node. + */ +Data::Data() {} + +/*! + * Returns the string representation of the node. + */ +QString Data::string() const { return u"Data"_s; } + /*! * @class Document * @brief The document class represents a JSON document node for use in error contexts. diff --git a/lib/core/src/qx-string.cpp b/lib/core/src/qx-string.cpp index da2227a4..27651811 100644 --- a/lib/core/src/qx-string.cpp +++ b/lib/core/src/qx-string.cpp @@ -345,27 +345,30 @@ QString String::mapArg(QAnyStringView s, const QMap& args, Qt: for(const QAnyStringView& view : resultBp.views()) { - // QString::arg() uses size() here instead of isEmpty(), so we keep consistent resultRaw = view.visit(qxFuncAggregate{ [resultRaw](QLatin1StringView v){ - if(v.size()) - { - auto fromLatin1 = QStringDecoder(QStringDecoder::Latin1, QStringDecoder::Flag::Stateless); - auto postAppend = fromLatin1.appendToBuffer(resultRaw, v); - Q_ASSERT(!fromLatin1.hasError()); - return postAppend; - } - return resultRaw + v.size(); + if(v.isEmpty()) + return resultRaw; + + thread_local auto fromLatin1 = QStringDecoder(QStringDecoder::Latin1, QStringDecoder::Flag::Stateless); + auto postAppend = fromLatin1.appendToBuffer(resultRaw, v); + Q_ASSERT(!fromLatin1.hasError()); + return postAppend; }, [resultRaw](QUtf8StringView v){ - auto fromUtf8 = QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless); + if(v.isEmpty()) + return resultRaw; + + thread_local auto fromUtf8 = QStringDecoder(QStringDecoder::Utf8, QStringDecoder::Flag::Stateless); auto postAppend = fromUtf8.appendToBuffer(resultRaw, v); Q_ASSERT(!fromUtf8.hasError()); return postAppend; }, [resultRaw](QStringView v){ - if(v.size()) - memcpy(resultRaw, v.data(), v.size() * sizeof(QChar)); + if(v.isEmpty()) + return resultRaw; + + memcpy(resultRaw, v.data(), v.size() * sizeof(QChar)); return resultRaw + v.size(); } }); @@ -379,4 +382,30 @@ QString String::mapArg(QAnyStringView s, const QMap& args, Qt: return result; } +/*! Capitalizes the first letter of every word in @a string, and ensures the rest of the letters + * are in lower case. This is not the same as Title Case, where some words are never capitalized. + * + * This function considers a 'word' to be distinct after any whitespace occurs. + */ +QString String::toHeadlineCase(const QString& string) +{ + QString hc(string); + + bool firstCh = true; + for(QChar& ch : hc) + { + if(ch.isSpace()) + firstCh = true; + else if(firstCh) + { + ch = ch.toUpper(); + firstCh = false; + } + else + ch = ch.toLower(); + } + + return hc; +} + }