diff --git a/src/grpc/CMakeLists.txt b/src/grpc/CMakeLists.txt index e39c01ce..beffeefc 100644 --- a/src/grpc/CMakeLists.txt +++ b/src/grpc/CMakeLists.txt @@ -11,6 +11,7 @@ qt_internal_add_module(Grpc qabstractgrpcchannel.h qabstractgrpcchannel_p.h qabstractgrpcchannel.cpp qgrpchttp2channel.h qgrpchttp2channel.cpp qgrpcclientbase.h qgrpcclientbase.cpp + qgrpccommonoptions_p.h qgrpccommonoptions.cpp qgrpccalloptions.h qgrpccalloptions.cpp qgrpcchanneloptions.h qgrpcchanneloptions.cpp qtgrpcglobal.h diff --git a/src/grpc/qgrpccalloptions.cpp b/src/grpc/qgrpccalloptions.cpp index a75c83cc..663b7704 100644 --- a/src/grpc/qgrpccalloptions.cpp +++ b/src/grpc/qgrpccalloptions.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include +#include #include #include @@ -23,13 +25,27 @@ using namespace Qt::StringLiterals; These options supersede the ones set via QGrpcChannelOptions. To configure the default options shared by RPCs, use QGrpcChannelOptions. + + \code + QGrpcCallOptions callOpts; + // Set the metadata for an individial RPC + callOpts.setMetadata({ + { "header" , "value1" }, + { "header" , "value2" }, + }); + const auto &md = callOpts.metadata(QtGrpc::MultiValue); + qDebug() << "Call Metadata: " << md; + + // Set a 2-second deadline for an individial RPC + callOpts.setDeadlineTimeout(2s); + qDebug() << "Call timeout: " << callOpts.deadlineTimeout(); + \endcode */ -class QGrpcCallOptionsPrivate : public QSharedData +class QGrpcCallOptionsPrivate : public QGrpcCommonOptions { public: - std::optional timeout; - QHash metadata; + QGrpcCallOptionsPrivate() = default; }; QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QGrpcCallOptionsPrivate) @@ -92,98 +108,152 @@ QGrpcCallOptions::operator QVariant() const */ /*! - Sets the \a timeout for a specific RPC and returns a reference to the - updated object. + \include qgrpccommonoptions.cpp set-deadline-timeout -//! [set-deadline-desc] - A deadline sets the limit for how long a client is willing to wait for a - response from a server. The actual deadline is computed by adding the \a - timeout to the start time of the RPC. + \note Setting this field \b{overrides} the corresponding channel options field + — see \l{QGrpcChannelOptions::setDeadlineTimeout()} - The deadline applies to the entire lifetime of an RPC, which includes - receiving the final QGrpcStatus for a previously started call and can thus - be unwanted for (long-lived) streams. -//! [set-deadline-desc] - - \note Setting this field overrides the value set by - QGrpcChannelOptions::setDeadline() for a specific RPC. + \sa deadlineTimeout() */ QGrpcCallOptions &QGrpcCallOptions::setDeadlineTimeout(std::chrono::milliseconds timeout) { - if (d_ptr->timeout == timeout) + if (d_ptr->deadlineTimeout() == timeout) return *this; d_ptr.detach(); Q_D(QGrpcCallOptions); - d->timeout = timeout; + d->setDeadlineTimeout(timeout); return *this; } +/*! + \include qgrpccommonoptions.cpp deadline-timeout +*/ +std::optional QGrpcCallOptions::deadlineTimeout() const noexcept +{ + Q_D(const QGrpcCallOptions); + return d->deadlineTimeout(); +} + +#if QT_DEPRECATED_SINCE(6, 13) + +/*! + \fn const QHash &QGrpcCallOptions::metadata() const & + \fn QHash QGrpcCallOptions::metadata() && + \deprecated [6.13] Use the QMultiHash overload instead. + + \include qgrpccommonoptions.cpp metadata + + \sa setMetadata() +*/ +const QHash &QGrpcCallOptions::metadata() const & noexcept +{ + Q_D(const QGrpcCallOptions); + return d->metadata(); +} +QHash QGrpcCallOptions::metadata() && +{ + Q_D(QGrpcCallOptions); + if (d->ref.loadRelaxed() != 1) + return d->metadata(); + return std::move(*d_ptr).metadata(); +} + /*! \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash &metadata) \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash &&metadata) + \deprecated [6.13] Use the QMultiHash overload instead. - Sets the client \a metadata for a specific RPC and returns a reference to the - updated object. + \include qgrpccommonoptions.cpp set-metadata -//! [set-metadata-desc] - QGrpcHttp2Channel converts the metadata into appropriate HTTP/2 headers - which will be added to the HTTP/2 request. -//! [set-metadata-desc] +//! [merge-md-note] + \note Call metadata is \b{merged} with any channel-level metadata when the + RPC starts — see +//! [merge-md-note] + \l{QGrpcChannelOptions::setMetadata(const QMultiHash&)}{QGrpcChannelOptions::setMetadata(QMultiHash)}. - \note Setting this field overrides the value set by - QGrpcChannelOptions::setMetadata() for a specific RPC. + \sa metadata() */ QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash &metadata) { - if (d_ptr->metadata == metadata) + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcCallOptions); - d->metadata = metadata; + d->setMetadata(metadata); return *this; } - QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash &&metadata) { - if (d_ptr->metadata == metadata) + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcCallOptions); - d->metadata = std::move(metadata); + d->setMetadata(std::move(metadata)); return *this; } -/*! - Returns the timeout duration that is used to calculate the deadline for a - specific RPC. - - If this field is unset, returns an empty \c {std::optional}. -*/ -std::optional QGrpcCallOptions::deadlineTimeout() const noexcept -{ - Q_D(const QGrpcCallOptions); - return d->timeout; -} +#endif // QT_DEPRECATED_SINCE(6, 13) /*! - \fn const QHash &QGrpcCallOptions::metadata() const & - \fn QHash QGrpcCallOptions::metadata() && + \since 6.10 + \fn const QMultiHash &QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) const & + \fn QMultiHash QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) && - Returns the client metadata for a specific RPC. - If this field is unset, returns empty metadata. + \include qgrpccommonoptions.cpp metadata-multi + + \sa {setMetadata(const QMultiHash&)}{setMetadata} */ -const QHash &QGrpcCallOptions::metadata() const & noexcept +const QMultiHash & +QGrpcCallOptions::metadata(QtGrpc::MultiValueTag tag) const & noexcept { Q_D(const QGrpcCallOptions); - return d->metadata; + return d->metadata(tag); } - -QHash QGrpcCallOptions::metadata() && +QMultiHash QGrpcCallOptions::metadata(QtGrpc::MultiValueTag tag) && { Q_D(QGrpcCallOptions); - if (d->ref.loadRelaxed() != 1) // return copy if shared - return { d->metadata }; - return std::move(d->metadata); + if (d->ref.loadRelaxed() != 1) + return d->metadata(tag); + return std::move(*d_ptr).metadata(tag); +} + +/*! + \since 6.10 + \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QMultiHash &metadata) + \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QMultiHash &&metadata) + \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(std::initializer_list> metadata) + + \include qgrpccommonoptions.cpp set-metadata-multi + + \include qgrpccalloptions.cpp merge-md-note + \l{QGrpcChannelOptions::setMetadata(const QMultiHash&)}{QGrpcChannelOptions::setMetadata(QMultiHash)}. + + \sa metadata(QtGrpc::MultiValueTag) +*/ +QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QMultiHash &metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcCallOptions); + d->setMetadata(metadata); + return *this; +} +QGrpcCallOptions &QGrpcCallOptions::setMetadata(QMultiHash &&metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcCallOptions); + d->setMetadata(std::move(metadata)); + return *this; +} +QGrpcCallOptions & +QGrpcCallOptions::setMetadata(std::initializer_list> list) +{ + return setMetadata(QMultiHash(list)); } #ifndef QT_NO_DEBUG_STREAM @@ -198,7 +268,7 @@ QDebug operator<<(QDebug debug, const QGrpcCallOptions &callOpts) const QDebugStateSaver save(debug); debug.nospace().noquote(); debug << "QGrpcCallOptions(deadline: " << callOpts.deadlineTimeout() - << ", metadata: " << callOpts.metadata() << ')'; + << ", metadata: " << callOpts.metadata(QtGrpc::MultiValue) << ')'; return debug; } #endif diff --git a/src/grpc/qgrpccalloptions.h b/src/grpc/qgrpccalloptions.h index 3eb4b4d9..98a09d68 100644 --- a/src/grpc/qgrpccalloptions.h +++ b/src/grpc/qgrpccalloptions.h @@ -5,11 +5,13 @@ #define QGRPCALLOPTIONS_H #include +#include #include #include #include #include +#include #include #include @@ -42,10 +44,24 @@ public: deadlineTimeout() const noexcept; Q_GRPC_EXPORT QGrpcCallOptions &setDeadlineTimeout(std::chrono::milliseconds timeout); +#if QT_DEPRECATED_SINCE(6, 13) + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT const QHash &metadata() const & noexcept; + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT QHash metadata() &&; + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QHash &metadata); + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QHash &&metadata); +#endif + [[nodiscard]] Q_GRPC_EXPORT const QMultiHash & + metadata(QtGrpc::MultiValueTag) const & noexcept; + [[nodiscard]] Q_GRPC_EXPORT QMultiHash + metadata(QtGrpc::MultiValueTag) &&; + Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QMultiHash &metadata); + Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QMultiHash &&metadata); + Q_GRPC_EXPORT QGrpcCallOptions & + setMetadata(std::initializer_list> list); private: QExplicitlySharedDataPointer d_ptr; diff --git a/src/grpc/qgrpcchanneloptions.cpp b/src/grpc/qgrpcchanneloptions.cpp index 37959b4b..2ec8c7da 100644 --- a/src/grpc/qgrpcchanneloptions.cpp +++ b/src/grpc/qgrpcchanneloptions.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include #include #include #include @@ -25,17 +26,34 @@ using namespace QtGrpc; to all remote procedure calls (RPCs) that operate on the associated channel, which is used to communicate with services. - Override options for specific RPCs with QGrcCallOptions. + Override options for specific RPCs with QGrpcCallOptions. + + \code + QGrpcChannelOptions channelOpts; + // Apply common metadata to every RPC + channelOpts.setMetadata({ + { "header" , "value1" }, + { "header" , "value2" }, + }); + const auto &md = channelOpts.metadata(QtGrpc::MultiValue); + qDebug() << "Channel Metadata: " << md; + + // Apply a 2-second deadline to every RPC + channelOpts.setDeadlineTimeout(2s); + qDebug() << "Channel timeout: " << channelOpts.deadlineTimeout(); + + // Configure SSL/TLS configuration + channelOpts.setSslConfiguration(QSslConfiguration()); + \endcode + \note It is up to the channel's implementation to determine the specifics of these options. */ -class QGrpcChannelOptionsPrivate : public QSharedData +class QGrpcChannelOptionsPrivate : public QGrpcCommonOptions { public: - std::optional timeout; - QHash metadata; QGrpcSerializationFormat serializationFormat; #if QT_CONFIG(ssl) std::optional sslConfiguration; @@ -101,57 +119,151 @@ QGrpcChannelOptions::operator QVariant() const } /*! - Sets the \a timeout for the channel and returns a reference to the updated - object. + \include qgrpccommonoptions.cpp set-deadline-timeout - \include qgrpccalloptions.cpp set-deadline-desc +//! [channel-note] + \note Setting this field applies to all RPCs that operate on the channel, + except those overriden by +//! [channel-note] + \l{QGrpcCallOptions::setDeadlineTimeout()} - \note The deadline set via the channel options applies to all RPCs that - operate on the channel, except those overridden by - QGrpcCallOptions::setDeadline(). + \sa deadlineTimeout() */ QGrpcChannelOptions &QGrpcChannelOptions::setDeadlineTimeout(std::chrono::milliseconds timeout) { - if (d_ptr->timeout == timeout) + if (d_ptr->deadlineTimeout() == timeout) return *this; d_ptr.detach(); Q_D(QGrpcChannelOptions); - d->timeout = timeout; + d->setDeadlineTimeout(timeout); return *this; } +#if QT_DEPRECATED_SINCE(6, 13) + +/*! + \fn const QHash &QGrpcChannelOptions::metadata() const & + \fn QHash QGrpcChannelOptions::metadata() && + \deprecated [6.13] Use \l{metadata(QtGrpc::MultiValueTag)}{metadata(QtGrpc::MultiValue)} instead. + + \include qgrpccommonoptions.cpp metadata + + \sa metadata(QtGrpc::MultiValueTag), setMetadata() +*/ +const QHash &QGrpcChannelOptions::metadata() const & noexcept +{ + Q_D(const QGrpcChannelOptions); + return d->metadata(); +} +QHash QGrpcChannelOptions::metadata() && +{ + Q_D(QGrpcChannelOptions); + if (d->ref.loadRelaxed() != 1) // return copy if shared + return d->metadata(); + return std::move(*d_ptr).metadata(); +} + /*! \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash &metadata) \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash &&metadata) + \deprecated [6.13] Use the QMultiHash overload instead. - Sets the client \a metadata for the channel and returns a reference to the - updated object. + \include qgrpccommonoptions.cpp set-metadata - \include qgrpccalloptions.cpp set-metadata-desc +//! [merge-md-note] + \note This metadata is included in every RPC made through the channel. + Channel metadata is \b{merged} with any call-specific metadata when the RPC + starts — see +//! [merge-md-note] + \l{QGrpcCallOptions::setMetadata(const QMultiHash&)}{QGrpcCallOptions::setMetadata(QMultiHash)} - \note The metadata set via the channel options applies to all RPCs that - operate on the channel, except those overridden by - QGrpcCallOptions::setMetadata(). + \sa metadata() */ QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash &metadata) { - if (d_ptr->metadata == metadata) + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcChannelOptions); - d->metadata = metadata; + d->setMetadata(metadata); + return *this; +} +QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash &&metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcChannelOptions); + d->setMetadata(std::move(metadata)); return *this; } -QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash &&metadata) +#endif // QT_DEPRECATED_SINCE(6, 13) + +/*! + \since 6.10 + \fn const QMultiHash &QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag) const & + \fn QMultiHash QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag) && + + \include qgrpccommonoptions.cpp metadata-multi + + \sa {setMetadata(const QMultiHash&)}{setMetadata} +*/ +const QMultiHash & +QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag tag) const & noexcept { - if (d_ptr->metadata == metadata) + Q_D(const QGrpcChannelOptions); + return d->metadata(tag); +} + +QMultiHash +QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag tag) && +{ + Q_D(QGrpcChannelOptions); + if (d->ref.loadRelaxed() != 1) + return d->metadata(tag); + return std::move(*d_ptr).metadata(tag); +} + +/*! + \since 6.10 + \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QMultiHash &metadata) + \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QMultiHash &&metadata) + \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(std::initializer_list> list) + + \include qgrpccommonoptions.cpp set-metadata-multi + + \include qgrpcchanneloptions.cpp merge-md-note + \l{QGrpcCallOptions::setMetadata(const QMultiHash&)}{QGrpcCallOptions::setMetadata(QMultiHash)} + + \sa metadata(QtGrpc::MultiValueTag) +*/ +QGrpcChannelOptions & +QGrpcChannelOptions::setMetadata(const QMultiHash &metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) return *this; d_ptr.detach(); Q_D(QGrpcChannelOptions); - d->metadata = std::move(metadata); + d->setMetadata(metadata); return *this; } +QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QMultiHash &&metadata) +{ + if (d_ptr->metadata(QtGrpc::MultiValue) == metadata) + return *this; + d_ptr.detach(); + Q_D(QGrpcChannelOptions); + d->setMetadata(std::move(metadata)); + return *this; +} +QGrpcChannelOptions & +QGrpcChannelOptions::setMetadata(std::initializer_list> list) +{ + return setMetadata(QMultiHash(list)); +} /*! \since 6.8 @@ -179,29 +291,7 @@ QGrpcChannelOptions::setSerializationFormat(const QGrpcSerializationFormat &form std::optional QGrpcChannelOptions::deadlineTimeout() const noexcept { Q_D(const QGrpcChannelOptions); - return d->timeout; -} - -/*! - \fn const QHash &QGrpcChannelOptions::metadata() const & - \fn QHash QGrpcChannelOptions::metadata() && - - Returns the client metadata for the channel. - - If this field is unset, returns empty metadata. -*/ -const QHash &QGrpcChannelOptions::metadata() const & noexcept -{ - Q_D(const QGrpcChannelOptions); - return d->metadata; -} - -QHash QGrpcChannelOptions::metadata() && -{ - Q_D(QGrpcChannelOptions); - if (d->ref.loadRelaxed() != 1) // return copy if shared - return { d->metadata }; - return std::move(d->metadata); + return d->deadlineTimeout(); } /*! @@ -258,7 +348,7 @@ QDebug operator<<(QDebug debug, const QGrpcChannelOptions &chOpts) const QDebugStateSaver save(debug); debug.nospace().noquote(); debug << "QGrpcChannelOptions(deadline: " << chOpts.deadlineTimeout() - << ", metadata: " << chOpts.metadata() + << ", metadata: " << chOpts.metadata(QtGrpc::MultiValue) << ", serializationFormat: " << chOpts.serializationFormat().suffix() << ", sslConfiguration: "; # if QT_CONFIG(ssl) diff --git a/src/grpc/qgrpcchanneloptions.h b/src/grpc/qgrpcchanneloptions.h index f8e1a8db..a6896dd2 100644 --- a/src/grpc/qgrpcchanneloptions.h +++ b/src/grpc/qgrpcchanneloptions.h @@ -5,6 +5,7 @@ #define QGRPCHANNELOPTIONS_H #include +#include #if QT_CONFIG(ssl) # include @@ -14,7 +15,7 @@ #include #include #include -#include +#include #include #include @@ -48,10 +49,25 @@ public: deadlineTimeout() const noexcept; Q_GRPC_EXPORT QGrpcChannelOptions &setDeadlineTimeout(std::chrono::milliseconds timeout); +#if QT_DEPRECATED_SINCE(6, 13) + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT const QHash &metadata() const & noexcept; + QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash") [[nodiscard]] Q_GRPC_EXPORT QHash metadata() &&; + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(const QHash &metadata); + QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload") Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QHash &&metadata); +#endif + [[nodiscard]] Q_GRPC_EXPORT const QMultiHash & + metadata(QtGrpc::MultiValueTag) const & noexcept; + [[nodiscard]] Q_GRPC_EXPORT QMultiHash + metadata(QtGrpc::MultiValueTag) &&; + Q_GRPC_EXPORT QGrpcChannelOptions & + setMetadata(const QMultiHash &metadata); + Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QMultiHash &&metadata); + Q_GRPC_EXPORT QGrpcChannelOptions & + setMetadata(std::initializer_list> list); [[nodiscard]] Q_GRPC_EXPORT QGrpcSerializationFormat serializationFormat() const; Q_GRPC_EXPORT QGrpcChannelOptions & diff --git a/src/grpc/qgrpccommonoptions.cpp b/src/grpc/qgrpccommonoptions.cpp new file mode 100644 index 00000000..9f237af5 --- /dev/null +++ b/src/grpc/qgrpccommonoptions.cpp @@ -0,0 +1,140 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +QT_BEGIN_NAMESPACE + +#if QT_DEPRECATED_SINCE(6, 13) + +namespace +{ + +inline QHash mergeHash(const QMultiHash &multiHash) +{ + QHash out; + for (const auto &key : multiHash.uniqueKeys()) + out.insert(key, multiHash.value(key)); + return out; +} + +} // namespace + +/*! +//! [metadata] + Returns the metadata. If this field is unset, returns empty + metadata. +//! [metadata] +*/ +const QHash &QGrpcCommonOptions::metadata() const & +{ + m_deprecatedQHashRefUsed = true; + if (m_metadataMulti != m_metadata) + m_metadata = mergeHash(m_metadataMulti); + return m_metadata; +} +QHash QGrpcCommonOptions::metadata() && +{ + if (m_metadataMulti != m_metadata) + m_metadata = mergeHash(m_metadataMulti); + return std::move(m_metadata); +} + +/*! +//! [set-metadata] + Sets the \a metadata and returns a reference to the updated object. + + When using QGrpcHttp2Channel, the metadata is converted to HTTP/2 headers + and added to the gRPC request. +//! [set-metadata] +*/ +void QGrpcCommonOptions::setMetadata(const QHash &md) +{ + if (m_deprecatedQHashRefUsed) + m_metadata = md; + m_metadataMulti = QMultiHash(md); +} +void QGrpcCommonOptions::setMetadata(QHash &&md) +{ + if (m_deprecatedQHashRefUsed) + m_metadata = md; + m_metadataMulti = QMultiHash(std::move(md)); +} + +#endif // QT_DEPRECATED_SINCE(6, 13) + +/*! +//! [deadline-timeout] + Returns the timeout duration that is used to calculate the deadline for RPCs. + + If this field is unset, returns an empty \c {std::optional}. +//! [deadline-timeout] +*/ +std::optional QGrpcCommonOptions::deadlineTimeout() const noexcept +{ + return m_timeout; +} + +/*! +//! [set-deadline-timeout] + Sets the \a timeout and returns a reference to the updated object. + + A deadline sets the limit for how long a client is willing to wait for a + response from a server. The actual deadline is computed by adding the \a + timeout to the start time of the RPC. + + The deadline applies to the entire lifetime of an RPC, which includes + receiving the final QGrpcStatus for a previously started call and can thus + be unwanted for (long-lived) streams. +//! [set-deadline-timeout] +*/ +void QGrpcCommonOptions::setDeadlineTimeout(std::chrono::milliseconds t) +{ + m_timeout = t; +} + +/*! +//! [metadata-multi] + \include qgrpccommonoptions.cpp metadata + Multiple values per key are supported. + + \code + const auto &md = opts.metadata(QtGrpc::MultiValue); + \endcode +//! [metadata-multi] +*/ +const QMultiHash & +QGrpcCommonOptions::metadata(QtGrpc::MultiValueTag /*tag*/) const & +{ + return m_metadataMulti; +} +QMultiHash QGrpcCommonOptions::metadata(QtGrpc::MultiValueTag /*tag*/) && +{ + return std::move(m_metadataMulti); +} + +/*! +//! [set-metadata-multi] + \include qgrpccommonoptions.cpp set-metadata + Multiple values per key are supported. +//! [set-metadata-multi] +*/ +void QGrpcCommonOptions::setMetadata(const QMultiHash &md) +{ + m_metadataMulti = md; +#if QT_DEPRECATED_SINCE(6, 13) + if (m_deprecatedQHashRefUsed) + m_metadata = mergeHash(m_metadataMulti); +#endif +} + +void QGrpcCommonOptions::setMetadata(QMultiHash &&md) +{ + m_metadataMulti = std::move(md); +#if QT_DEPRECATED_SINCE(6, 13) + if (m_deprecatedQHashRefUsed) + m_metadata = mergeHash(m_metadataMulti); +#endif +} + +QT_END_NAMESPACE diff --git a/src/grpc/qgrpccommonoptions_p.h b/src/grpc/qgrpccommonoptions_p.h new file mode 100644 index 00000000..85e2591a --- /dev/null +++ b/src/grpc/qgrpccommonoptions_p.h @@ -0,0 +1,80 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef QTGRPCCOMMONOPTIONS_P_H +#define QTGRPCCOMMONOPTIONS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +#include +#include +#include + +#include +#include + +QT_BEGIN_NAMESPACE + +class QGrpcCommonOptions : public QSharedData +{ +public: + QGrpcCommonOptions() = default; + virtual ~QGrpcCommonOptions() = default; + + [[nodiscard]] std::optional deadlineTimeout() const noexcept; + void setDeadlineTimeout(std::chrono::milliseconds t); + +#if QT_DEPRECATED_SINCE(6, 13) + const QHash &metadata() const &; + QHash metadata() &&; + void setMetadata(const QHash &md); + void setMetadata(QHash &&md); +#endif + const QMultiHash &metadata(QtGrpc::MultiValueTag /*tag*/) const &; + QMultiHash metadata(QtGrpc::MultiValueTag /*tag*/) &&; + void setMetadata(const QMultiHash &md); + void setMetadata(QMultiHash &&md); + +private: + std::optional m_timeout; + QMultiHash m_metadataMulti; +#if QT_DEPRECATED_SINCE(6, 13) + mutable QHash m_metadata; + mutable bool m_deprecatedQHashRefUsed = false; +#endif +}; + +inline bool operator==(const QMultiHash &multiHash, + const QHash &hash) +{ + if (hash.size() != multiHash.size()) + return false; + for (const auto &[k, v] : hash.asKeyValueRange()) { + const auto [f, l] = multiHash.equal_range(k); + if (f == l || std::next(f) != l || *f != v) + return false; + } + return true; +} + +inline bool operator!=(const QMultiHash &multiHash, + const QHash &hash) +{ + return !(multiHash == hash); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/grpc/qgrpchttp2channel.cpp b/src/grpc/qgrpchttp2channel.cpp index abb9162a..4cc1a000 100644 --- a/src/grpc/qgrpchttp2channel.cpp +++ b/src/grpc/qgrpchttp2channel.cpp @@ -553,8 +553,8 @@ void Http2Handler::prepareInitialRequest(QGrpcOperationContext *operationContext } }; - iterateMetadata(channelOptions.metadata()); - iterateMetadata(operationContext->callOptions().metadata()); + iterateMetadata(channelOptions.metadata(QtGrpc::MultiValue)); + iterateMetadata(operationContext->callOptions().metadata(QtGrpc::MultiValue)); writeMessage(operationContext->argument()); } @@ -688,8 +688,8 @@ QGrpcHttp2ChannelPrivate::QGrpcHttp2ChannelPrivate(const QUrl &uri, QGrpcHttp2Ch : defaultContentType; bool warnAboutFormatConflict = !formatSuffix.isEmpty(); - const auto it = channelOptions.metadata().constFind(ContentTypeHeader.data()); - if (it != channelOptions.metadata().cend()) { + const auto it = channelOptions.metadata(QtGrpc::MultiValue).constFind(ContentTypeHeader.data()); + if (it != channelOptions.metadata(QtGrpc::MultiValue).cend()) { if (formatSuffix.isEmpty() && it.value() != DefaultContentType) { if (it.value() == "application/grpc+json") { channelOptions.setSerializationFormat(SerializationFormat::Json); diff --git a/src/grpc/qtgrpcnamespace.h b/src/grpc/qtgrpcnamespace.h index a4567cc2..225d21ef 100644 --- a/src/grpc/qtgrpcnamespace.h +++ b/src/grpc/qtgrpcnamespace.h @@ -47,6 +47,14 @@ enum class StatusCode : quint8 { }; Q_ENUM_NS(StatusCode) +// ### Qt7: remove QHash metadata interfaces. +enum class MultiValueTag : quint8 { Allow QT_DEPRECATED_X("use QtGrpc::MultiValue") }; +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +QT_WARNING_DISABLE_CLANG("-Wunused-const-variable") +inline static constexpr auto MultiValue = QtGrpc::MultiValueTag::Allow; +QT_WARNING_POP + Q_CLASSINFO("RegisterEnumClassesUnscoped", "false") } // namespace QtGrpc diff --git a/src/grpc/qtgrpcnamespace.qdoc b/src/grpc/qtgrpcnamespace.qdoc index f277531e..93724c44 100644 --- a/src/grpc/qtgrpcnamespace.qdoc +++ b/src/grpc/qtgrpcnamespace.qdoc @@ -82,3 +82,21 @@ \sa{https://github.com/grpc/grpc/blob/master/doc/statuscodes.md}{gRPC status codes} */ +/*! + \since 6.10 + \enum QtGrpc::MultiValueTag + \brief Tag type used to access QMultiHash metadata. + \omitvalue Allow + \note This type is an implementation detail. Use QtGrpc::MultiValue for public API access. + + \sa QtGrpc::MultiValue, QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag), + QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) +*/ + +/*! + \since 6.10 + \variable QtGrpc::MultiValue + \brief Tag used to access QMultiHash metadata. + \sa QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag), + QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) +*/ diff --git a/src/grpcquick/qqmlgrpcmetadata_p.h b/src/grpcquick/qqmlgrpcmetadata_p.h index 3ca8f19e..5945fb99 100644 --- a/src/grpcquick/qqmlgrpcmetadata_p.h +++ b/src/grpcquick/qqmlgrpcmetadata_p.h @@ -37,7 +37,7 @@ public: explicit QQmlGrpcMetadata(QObject *parent = nullptr); ~QQmlGrpcMetadata() override; - const QHash &metadata() const & noexcept { return m_metadata; } + const QMultiHash &metadata() const & noexcept { return m_metadata; } void metadata() && = delete; const QVariantMap &data() const { return m_variantdata; } @@ -48,7 +48,7 @@ Q_SIGNALS: private: QVariantMap m_variantdata; - QHash m_metadata; + QMultiHash m_metadata; Q_DISABLE_COPY_MOVE(QQmlGrpcMetadata) }; diff --git a/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt b/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt index 222d84aa..21837c30 100644 --- a/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt +++ b/tests/auto/grpc/qgrpccalloptions/CMakeLists.txt @@ -10,6 +10,8 @@ endif() qt_internal_add_test(tst_qgrpccalloptions SOURCES tst_qgrpccalloptions.cpp + INCLUDE_DIRECTORIES + "${CMAKE_CURRENT_LIST_DIR}/../shared" LIBRARIES Qt::Core Qt::Test diff --git a/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp b/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp index 74a509a4..626e217a 100644 --- a/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp +++ b/tests/auto/grpc/qgrpccalloptions/tst_qgrpccalloptions.cpp @@ -1,129 +1,32 @@ // Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include + #include #include -#include - -using namespace std::chrono_literals; - class QGrpcCallOptionsTest : public QObject { Q_OBJECT private Q_SLOTS: - void hasSpecialMemberFunctions() const; - void hasImplicitQVariant() const; - void hasMemberSwap() const; - void propertyMetadata() const; - void propertyDeadline() const; - void streamsToDebug() const; + void hasSpecialMemberFunctions() const { common.hasSpecialMemberFunctions(); } + void hasImplicitQVariant() const { common.hasImplicitQVariant(); } + void hasMemberSwap() const { common.hasMemberSwap(); } +#if QT_DEPRECATED_SINCE(6, 13) + void deprecatedPropertyMetadata() const { common.deprecatedPropertyMetadata(); } + void propertyMetadataCompat() const { common.propertyMetadataCompat(); } +#endif + void propertyMetadata() const { common.propertyMetadata(); } + void propertyDeadline() const { common.propertyDeadline(); } + void streamsToDebug() const { common.streamsToDebug(); } + +private: + GrpcCommonOptionsTest common; }; -void QGrpcCallOptionsTest::hasSpecialMemberFunctions() const -{ - QGrpcCallOptions o1; - QVERIFY(!o1.deadlineTimeout()); - QVERIFY(o1.metadata().empty()); - - o1.setDeadlineTimeout(100ms); - - QGrpcCallOptions o2(o1); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); - - QGrpcCallOptions o3 = o1; - QCOMPARE_EQ(o1.deadlineTimeout(), o3.deadlineTimeout()); - - QGrpcCallOptions o4(std::move(o1)); - QCOMPARE_EQ(o4.deadlineTimeout(), o2.deadlineTimeout()); - - o1 = std::move(o4); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::hasImplicitQVariant() const -{ - QGrpcCallOptions o1; - o1.setDeadlineTimeout(250ms); - o1.setMetadata({ - { "keyA", "valA" }, - { "keyB", "valB" }, - }); - - QVariant v = o1; - QCOMPARE_EQ(v.metaType(), QMetaType::fromType()); - const auto o2 = v.value(); - QCOMPARE_EQ(o1.metadata(), o2.metadata()); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::hasMemberSwap() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcCallOptions o1; - o1.setDeadlineTimeout(Dur); - QGrpcCallOptions o2; - - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); - o2.swap(o1); - QCOMPARE_EQ(o2.deadlineTimeout(), Dur); - QVERIFY(!o1.deadlineTimeout()); - swap(o2, o1); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::propertyMetadata() const -{ - QHash md = { - { "keyA", "valA" }, - { "keyB", "valB" }, - }; - - QGrpcCallOptions o1; - auto o1Detach = o1; - o1.setMetadata(md); - QCOMPARE_EQ(o1.metadata(), md); - QCOMPARE_NE(o1.metadata(), o1Detach.metadata()); - - QGrpcCallOptions o2; - auto o2Detach = o2; - o2.setMetadata(std::move(md)); - QCOMPARE_EQ(o2.metadata(), o1.metadata()); - QCOMPARE_NE(o2.metadata(), o2Detach.metadata()); - - QCOMPARE_EQ(std::move(o1).metadata(), o2.metadata()); -} - -void QGrpcCallOptionsTest::propertyDeadline() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcCallOptions o1; - auto o1Detach = o1; - o1.setDeadlineTimeout(Dur); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QCOMPARE_NE(o1.deadlineTimeout(), o1Detach.deadlineTimeout()); -} - -void QGrpcCallOptionsTest::streamsToDebug() const -{ - QGrpcCallOptions o; - QString storage; - QDebug dbg(&storage); - dbg.noquote().nospace(); - - dbg << o; - QVERIFY(!storage.isEmpty()); - - std::unique_ptr ustr(QTest::toString(o)); - QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get())); -} - QTEST_MAIN(QGrpcCallOptionsTest) #include "tst_qgrpccalloptions.moc" diff --git a/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt b/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt index 643fb118..6ba70100 100644 --- a/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt +++ b/tests/auto/grpc/qgrpcchanneloptions/CMakeLists.txt @@ -10,6 +10,8 @@ endif() qt_internal_add_test(tst_qgrpcchanneloptions SOURCES tst_qgrpcchanneloptions.cpp + INCLUDE_DIRECTORIES + "${CMAKE_CURRENT_LIST_DIR}/../shared" LIBRARIES Qt::Core Qt::Test diff --git a/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp b/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp index df261c00..34865861 100644 --- a/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp +++ b/tests/auto/grpc/qgrpcchanneloptions/tst_qgrpcchanneloptions.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include + #include #include @@ -8,116 +10,31 @@ #include -using namespace std::chrono_literals; - class QGrpcChannelOptionsTest : public QObject { Q_OBJECT private Q_SLOTS: - void hasSpecialMemberFunctions() const; - void hasImplicitQVariant() const; - void hasMemberSwap() const; - void propertyMetadata() const; - void propertyDeadline() const; + void hasSpecialMemberFunctions() const { common.hasSpecialMemberFunctions(); } + void hasImplicitQVariant() const { common.hasImplicitQVariant(); } + void hasMemberSwap() const { common.hasMemberSwap(); } +#if QT_DEPRECATED_SINCE(6, 13) + void deprecatedPropertyMetadata() const { common.deprecatedPropertyMetadata(); } + void propertyMetadataCompat() const { common.propertyMetadataCompat(); } +#endif + void propertyMetadata() const { common.propertyMetadata(); } + void propertyDeadline() const { common.propertyDeadline(); } + void streamsToDebug() const { common.streamsToDebug(); } + void propertySerializationFormat() const; #if QT_CONFIG(ssl) void propertySslConfiguration() const; #endif - void streamsToDebug() const; + +private: + GrpcCommonOptionsTest common; }; -void QGrpcChannelOptionsTest::hasSpecialMemberFunctions() const -{ - QGrpcChannelOptions o1; - QVERIFY(!o1.deadlineTimeout()); - QVERIFY(o1.metadata().empty()); - -#if QT_CONFIG(ssl) - QVERIFY(!o1.sslConfiguration()); -#endif - - o1.setDeadlineTimeout(100ms); - - QGrpcChannelOptions o2(o1); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); - - QGrpcChannelOptions o3 = o1; - QCOMPARE_EQ(o1.deadlineTimeout(), o3.deadlineTimeout()); - - QGrpcChannelOptions o4(std::move(o1)); - QCOMPARE_EQ(o4.deadlineTimeout(), o2.deadlineTimeout()); - - o1 = std::move(o4); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcChannelOptionsTest::hasImplicitQVariant() const -{ - QGrpcChannelOptions o1; - o1.setDeadlineTimeout(250ms); - o1.setMetadata({ - { "keyA", "valA" }, - { "keyB", "valB" }, - }); - - QVariant v = o1; - QCOMPARE_EQ(v.metaType(), QMetaType::fromType()); - const auto o2 = v.value(); - QCOMPARE_EQ(o1.metadata(), o2.metadata()); - QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); -} - -void QGrpcChannelOptionsTest::hasMemberSwap() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcChannelOptions o1; - o1.setDeadlineTimeout(Dur); - QGrpcChannelOptions o2; - - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); - o2.swap(o1); - QCOMPARE_EQ(o2.deadlineTimeout(), Dur); - swap(o2, o1); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QVERIFY(!o2.deadlineTimeout()); -} - -void QGrpcChannelOptionsTest::propertyMetadata() const -{ - QHash md = { - { "keyA", "valA" }, - { "keyB", "valB" }, - }; - - QGrpcChannelOptions o1; - auto o1Detach = o1; - o1.setMetadata(md); - QCOMPARE_EQ(o1.metadata(), md); - QCOMPARE_NE(o1.metadata(), o1Detach.metadata()); - - QGrpcChannelOptions o2; - auto o2Detach = o2; - o2.setMetadata(std::move(md)); - QCOMPARE_EQ(o2.metadata(), o1.metadata()); - QCOMPARE_NE(o1.metadata(), o2Detach.metadata()); - - QCOMPARE_EQ(std::move(o1).metadata(), o2.metadata()); -} - -void QGrpcChannelOptionsTest::propertyDeadline() const -{ - constexpr std::chrono::milliseconds Dur = 50ms; - - QGrpcChannelOptions o1; - auto o1Detach = o1; - o1.setDeadlineTimeout(Dur); - QCOMPARE_EQ(o1.deadlineTimeout(), Dur); - QCOMPARE_NE(o1.deadlineTimeout(), o1Detach.deadlineTimeout()); -} - void QGrpcChannelOptionsTest::propertySerializationFormat() const { QGrpcSerializationFormat fmt(QtGrpc::SerializationFormat::Json); @@ -147,20 +64,6 @@ void QGrpcChannelOptionsTest::propertySslConfiguration() const } #endif -void QGrpcChannelOptionsTest::streamsToDebug() const -{ - QGrpcChannelOptions o; - QString storage; - QDebug dbg(&storage); - dbg.nospace().noquote(); - - dbg << o; - QVERIFY(!storage.isEmpty()); - - std::unique_ptr ustr(QTest::toString(o)); - QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get())); -} - QTEST_MAIN(QGrpcChannelOptionsTest) #include "tst_qgrpcchanneloptions.moc" diff --git a/tests/auto/grpc/shared/grpccommonoptions.h b/tests/auto/grpc/shared/grpccommonoptions.h new file mode 100644 index 00000000..689db084 --- /dev/null +++ b/tests/auto/grpc/shared/grpccommonoptions.h @@ -0,0 +1,237 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +#include + +#include +#include +#include + +using namespace std::chrono_literals; + +template +class GrpcCommonOptionsTest +{ +public: + void hasSpecialMemberFunctions() const + { + T o1; + QVERIFY(!o1.deadlineTimeout()); + QVERIFY(o1.metadata(QtGrpc::MultiValue).empty()); + + o1.setDeadlineTimeout(100ms); + + T o2(o1); + QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); + + T o3 = o1; + QCOMPARE_EQ(o1.deadlineTimeout(), o3.deadlineTimeout()); + + T o4(std::move(o1)); + QCOMPARE_EQ(o4.deadlineTimeout(), o2.deadlineTimeout()); + + o1 = std::move(o4); + QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); + } + void hasImplicitQVariant() const + { + T o1; + o1.setDeadlineTimeout(250ms); + o1.setMetadata({ + { "keyA", "valA" }, + { "keyB", "valB" }, + }); + + QVariant v = o1; + QCOMPARE_EQ(v.metaType(), QMetaType::fromType()); + const auto o2 = v.value(); + QCOMPARE_EQ(o1.metadata(QtGrpc::MultiValue), o2.metadata(QtGrpc::MultiValue)); + QCOMPARE_EQ(o1.deadlineTimeout(), o2.deadlineTimeout()); + } + void hasMemberSwap() const + { + constexpr std::chrono::milliseconds Dur = 50ms; + + T o1; + o1.setDeadlineTimeout(Dur); + T o2; + + QCOMPARE_EQ(o1.deadlineTimeout(), Dur); + QVERIFY(!o2.deadlineTimeout()); + o2.swap(o1); + QCOMPARE_EQ(o2.deadlineTimeout(), Dur); + QVERIFY(!o1.deadlineTimeout()); + swap(o2, o1); + QCOMPARE_EQ(o1.deadlineTimeout(), Dur); + QVERIFY(!o2.deadlineTimeout()); + } + +#if QT_DEPRECATED_SINCE(6, 13) +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED + + void deprecatedPropertyMetadata() const + { + QHash data = { + { "keyA", "valA" }, + { "keyB", "valB" }, + }; + + // cref setter + T o1; + auto o1Detach = o1; + o1.setMetadata(data); + QCOMPARE_EQ(o1.metadata(), data); + QCOMPARE_NE(o1.metadata(), o1Detach.metadata()); + + // rvalue setter + T o2; + auto o2Detach = o2; + auto dataMoved = data; + o2.setMetadata(std::move(dataMoved)); + QCOMPARE_EQ(o2.metadata(), data); + QCOMPARE_NE(o2.metadata(), o2Detach.metadata()); + + // rvalue-this getter + T o3; + o3.setMetadata(data); + auto movedMd = std::move(o3).metadata(); + QCOMPARE_EQ(movedMd, data); + } + void propertyMetadataCompat() const + { + auto toMulti = [](const QHash &m) { + return QMultiHash(m); + }; + QMultiHash multiMd = { + { "keyA", "valA1" }, + { "keyA", "valA2" }, + { "keyB", "valB1" }, + { "keyB", "valB2" }, + { "keyC", "valC" }, + }; + QHash md = { + { "keyA", "valA2" }, + { "keyB", "valB2" }, + { "keyC", "valC" }, + }; + + T o1; + o1.setMetadata(md); + + const auto &mdRef = o1.metadata(); + const auto &multiMdRef = o1.metadata(QtGrpc::MultiValue); + + QCOMPARE_EQ(mdRef, md); + QCOMPARE_EQ(multiMdRef, toMulti(mdRef)); + QCOMPARE_NE(typeid(mdRef), typeid(multiMdRef)); + + // Check that the handed out reference gets updates for QMultiHash setter + o1.setMetadata(multiMd); + QCOMPARE_EQ(multiMdRef, multiMd); + QCOMPARE_EQ(mdRef, md); + multiMd.insert("keyD", "valD"); + o1.setMetadata(multiMd); + QCOMPARE_EQ(multiMdRef, multiMd); + QCOMPARE_NE(mdRef, md); + md.insert("keyD", "valD"); + QCOMPARE_EQ(mdRef, md); + // Check that the handed out reference gets updates for QHash setter + o1.setMetadata(md); + QCOMPARE_EQ(multiMdRef, toMulti(md)); + QCOMPARE_EQ(mdRef, md); + + // Check shared state mutation due to lazy evaluation in metadata() + auto mdCopy = md; + T o2; + o2.setMetadata(mdCopy); + auto o2Detach = o2; + const auto &o2Md = o2.metadata(); + const auto &o2DetachMd = o2Detach.metadata(); + QCOMPARE_EQ(o2Md, mdCopy); + QCOMPARE_EQ(o2Md, o2DetachMd); + mdCopy.insert("keyX", "valX"); + o2.setMetadata(mdCopy); // trigger new merge + const auto &o2MdAfter = o2.metadata(); + const auto &o2DetachMdAfter = o2Detach.metadata(); + QCOMPARE_NE(o2MdAfter, o2DetachMdAfter); + + T o3A; + T o3B = o3A; + const auto &o3aMd = o3A.metadata(); + o3B.setMetadata(QHash{ + {"keyA","valA"}, {"keyB","valB"} + }); + // o3aMd is not affected by the update since o3B deprecatedQHashRef is used + QCOMPARE_NE(o3aMd, o3B.metadata()); + o3A.setMetadata(QHash{ + {"keyA","valA"}, {"keyB","valB"}, {"keyC","valC"}, + }); + // o3aMd is updated accoringly though + QCOMPARE_EQ(o3aMd, o3A.metadata()); + QCOMPARE_NE(o3B.metadata(), o3A.metadata()); + } + +QT_WARNING_POP +#endif + void propertyMetadata() const + { + std::initializer_list> list = { + { "keyA", "valA1" }, + { "keyA", "valA2" }, + { "keyB", "valB" }, + }; + QMultiHash data(list); + + // cref setter + T o1; + auto o1Detach = o1; + o1.setMetadata(data); + QCOMPARE_EQ(o1.metadata(QtGrpc::MultiValue), data); + QCOMPARE_NE(o1.metadata(QtGrpc::MultiValue), o1Detach.metadata(QtGrpc::MultiValue)); + + // rvalue setter + T o2; + auto o2Detach = o2; + auto dataMoved = data; + o2.setMetadata(std::move(dataMoved)); + QCOMPARE_EQ(o2.metadata(QtGrpc::MultiValue), data); + QCOMPARE_NE(o2.metadata(QtGrpc::MultiValue), o2Detach.metadata(QtGrpc::MultiValue)); + + // rvalue-this getter + T o3; + o3.setMetadata(data); + auto movedMd = std::move(o3).metadata(QtGrpc::MultiValue); + QCOMPARE_EQ(movedMd, data); + + // std::initializer_list setter + T o4; + o4.setMetadata(list); + QCOMPARE_EQ(o4.metadata(QtGrpc::MultiValue), data); + } + void propertyDeadline() const + { + constexpr std::chrono::milliseconds Dur = 50ms; + + T o1; + auto o1Detach = o1; + o1.setDeadlineTimeout(Dur); + QCOMPARE_EQ(o1.deadlineTimeout(), Dur); + QCOMPARE_NE(o1.deadlineTimeout(), o1Detach.deadlineTimeout()); + } + void streamsToDebug() const + { + T o; + QString storage; + QDebug dbg(&storage); + dbg.noquote().nospace(); + + dbg << o; + QVERIFY(!storage.isEmpty()); + + std::unique_ptr ustr(QTest::toString(o)); + QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get())); + } +};