Deprecate QHash<QBA,QBA> metadata in favor of QMultiHash

Even though gRPC provides some leeway, as "Access to metadata is
language dependent", we have yet to see an implementation which doesn't
support multiple values per key.

Common implementations like grpc-c++, grpc-go or grpc-java provide this
and we should do it aswell.

Ref: https://grpc.io/docs/what-is-grpc/core-concepts/#metadata
Ref: https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-metadata.md#constructing-metadata

Use a base class to implement common functionality for the options
classes. Also use a new common class in the tests.

The QMultiHash overload is selected when the 'QtGrpc::MultiValue'
argument is used in 'metadata' calls. We update the documentation
accordingly.

Deprecation is scheduled for Qt 6.13

Users with a custom Qt build - those who care - should be rewarded with
minimal traces of this "accident" and should not suffer from potential
performance/storage overhead. Therefore we deprecate QtGrpc::MultiValue
for those builds, effectively providing Qt 7 behavior.

[ChangeLog][Deprecation Notice]
Deprecate the metadata()/setMetadata() methods on QGrpcCallOptions and
QGrpcChannelOptions that use QHash in favor of the new overloads that
use QMultiHash. This is more in line with the gRPC specification.

Fixes: QTBUG-136471
Change-Id: I58d14d2c304c06de269c99ba5383beee86d12f77
Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
This commit is contained in:
Dennis Oberst 2025-04-24 16:14:59 +02:00
parent 9a1cc11111
commit 778371b8ea
16 changed files with 819 additions and 333 deletions

View File

@ -11,6 +11,7 @@ qt_internal_add_module(Grpc
qabstractgrpcchannel.h qabstractgrpcchannel_p.h qabstractgrpcchannel.cpp qabstractgrpcchannel.h qabstractgrpcchannel_p.h qabstractgrpcchannel.cpp
qgrpchttp2channel.h qgrpchttp2channel.cpp qgrpchttp2channel.h qgrpchttp2channel.cpp
qgrpcclientbase.h qgrpcclientbase.cpp qgrpcclientbase.h qgrpcclientbase.cpp
qgrpccommonoptions_p.h qgrpccommonoptions.cpp
qgrpccalloptions.h qgrpccalloptions.cpp qgrpccalloptions.h qgrpccalloptions.cpp
qgrpcchanneloptions.h qgrpcchanneloptions.cpp qgrpcchanneloptions.h qgrpcchanneloptions.cpp
qtgrpcglobal.h qtgrpcglobal.h

View File

@ -1,6 +1,8 @@
// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGrpc/private/qgrpccommonoptions_p.h>
#include <QtGrpc/private/qtgrpclogging_p.h>
#include <QtGrpc/qgrpccalloptions.h> #include <QtGrpc/qgrpccalloptions.h>
#include <QtCore/qbytearray.h> #include <QtCore/qbytearray.h>
@ -23,13 +25,27 @@ using namespace Qt::StringLiterals;
These options supersede the ones set via QGrpcChannelOptions. These options supersede the ones set via QGrpcChannelOptions.
To configure the default options shared by RPCs, use 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: public:
std::optional<std::chrono::milliseconds> timeout; QGrpcCallOptionsPrivate() = default;
QHash<QByteArray, QByteArray> metadata;
}; };
QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QGrpcCallOptionsPrivate) 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 \include qgrpccommonoptions.cpp set-deadline-timeout
updated object.
//! [set-deadline-desc] \note Setting this field \b{overrides} the corresponding channel options field
A deadline sets the limit for how long a client is willing to wait for a see \l{QGrpcChannelOptions::setDeadlineTimeout()}
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 \sa deadlineTimeout()
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.
*/ */
QGrpcCallOptions &QGrpcCallOptions::setDeadlineTimeout(std::chrono::milliseconds timeout) QGrpcCallOptions &QGrpcCallOptions::setDeadlineTimeout(std::chrono::milliseconds timeout)
{ {
if (d_ptr->timeout == timeout) if (d_ptr->deadlineTimeout() == timeout)
return *this; return *this;
d_ptr.detach(); d_ptr.detach();
Q_D(QGrpcCallOptions); Q_D(QGrpcCallOptions);
d->timeout = timeout; d->setDeadlineTimeout(timeout);
return *this; return *this;
} }
/*!
\include qgrpccommonoptions.cpp deadline-timeout
*/
std::optional<std::chrono::milliseconds> QGrpcCallOptions::deadlineTimeout() const noexcept
{
Q_D(const QGrpcCallOptions);
return d->deadlineTimeout();
}
#if QT_DEPRECATED_SINCE(6, 13)
/*!
\fn const QHash<QByteArray, QByteArray> &QGrpcCallOptions::metadata() const &
\fn QHash<QByteArray, QByteArray> QGrpcCallOptions::metadata() &&
\deprecated [6.13] Use the QMultiHash overload instead.
\include qgrpccommonoptions.cpp metadata
\sa setMetadata()
*/
const QHash<QByteArray, QByteArray> &QGrpcCallOptions::metadata() const & noexcept
{
Q_D(const QGrpcCallOptions);
return d->metadata();
}
QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> &metadata) \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash<QByteArray, QByteArray> &metadata)
\fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata) \fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata)
\deprecated [6.13] Use the QMultiHash overload instead.
Sets the client \a metadata for a specific RPC and returns a reference to the \include qgrpccommonoptions.cpp set-metadata
updated object.
//! [set-metadata-desc] //! [merge-md-note]
QGrpcHttp2Channel converts the metadata into appropriate HTTP/2 headers \note Call metadata is \b{merged} with any channel-level metadata when the
which will be added to the HTTP/2 request. RPC starts see
//! [set-metadata-desc] //! [merge-md-note]
\l{QGrpcChannelOptions::setMetadata(const QMultiHash<QByteArray,
QByteArray>&)}{QGrpcChannelOptions::setMetadata(QMultiHash)}.
\note Setting this field overrides the value set by \sa metadata()
QGrpcChannelOptions::setMetadata() for a specific RPC.
*/ */
QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash<QByteArray, QByteArray> &metadata) QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QHash<QByteArray, QByteArray> &metadata)
{ {
if (d_ptr->metadata == metadata) if (d_ptr->metadata(QtGrpc::MultiValue) == metadata)
return *this; return *this;
d_ptr.detach(); d_ptr.detach();
Q_D(QGrpcCallOptions); Q_D(QGrpcCallOptions);
d->metadata = metadata; d->setMetadata(metadata);
return *this; return *this;
} }
QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata) QGrpcCallOptions &QGrpcCallOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata)
{ {
if (d_ptr->metadata == metadata) if (d_ptr->metadata(QtGrpc::MultiValue) == metadata)
return *this; return *this;
d_ptr.detach(); d_ptr.detach();
Q_D(QGrpcCallOptions); Q_D(QGrpcCallOptions);
d->metadata = std::move(metadata); d->setMetadata(std::move(metadata));
return *this; return *this;
} }
/*! #endif // QT_DEPRECATED_SINCE(6, 13)
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<std::chrono::milliseconds> QGrpcCallOptions::deadlineTimeout() const noexcept
{
Q_D(const QGrpcCallOptions);
return d->timeout;
}
/*! /*!
\fn const QHash<QByteArray, QByteArray> &QGrpcCallOptions::metadata() const & \since 6.10
\fn QHash<QByteArray, QByteArray> QGrpcCallOptions::metadata() && \fn const QMultiHash<QByteArray, QByteArray> &QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) const &
\fn QMultiHash<QByteArray, QByteArray> QGrpcCallOptions::metadata(QtGrpc::MultiValueTag) &&
Returns the client metadata for a specific RPC. \include qgrpccommonoptions.cpp metadata-multi
If this field is unset, returns empty metadata.
\sa {setMetadata(const QMultiHash<QByteArray, QByteArray>&)}{setMetadata}
*/ */
const QHash<QByteArray, QByteArray> &QGrpcCallOptions::metadata() const & noexcept const QMultiHash<QByteArray, QByteArray> &
QGrpcCallOptions::metadata(QtGrpc::MultiValueTag tag) const & noexcept
{ {
Q_D(const QGrpcCallOptions); Q_D(const QGrpcCallOptions);
return d->metadata; return d->metadata(tag);
} }
QMultiHash<QByteArray, QByteArray> QGrpcCallOptions::metadata(QtGrpc::MultiValueTag tag) &&
QHash<QByteArray, QByteArray> QGrpcCallOptions::metadata() &&
{ {
Q_D(QGrpcCallOptions); Q_D(QGrpcCallOptions);
if (d->ref.loadRelaxed() != 1) // return copy if shared if (d->ref.loadRelaxed() != 1)
return { d->metadata }; return d->metadata(tag);
return std::move(d->metadata); return std::move(*d_ptr).metadata(tag);
}
/*!
\since 6.10
\fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QMultiHash<QByteArray, QByteArray> &metadata)
\fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(QMultiHash<QByteArray, QByteArray> &&metadata)
\fn QGrpcCallOptions &QGrpcCallOptions::setMetadata(std::initializer_list<std::pair<QByteArray, QByteArray>> metadata)
\include qgrpccommonoptions.cpp set-metadata-multi
\include qgrpccalloptions.cpp merge-md-note
\l{QGrpcChannelOptions::setMetadata(const QMultiHash<QByteArray,
QByteArray>&)}{QGrpcChannelOptions::setMetadata(QMultiHash)}.
\sa metadata(QtGrpc::MultiValueTag)
*/
QGrpcCallOptions &QGrpcCallOptions::setMetadata(const QMultiHash<QByteArray, QByteArray> &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<QByteArray, QByteArray> &&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<std::pair<QByteArray, QByteArray>> list)
{
return setMetadata(QMultiHash<QByteArray, QByteArray>(list));
} }
#ifndef QT_NO_DEBUG_STREAM #ifndef QT_NO_DEBUG_STREAM
@ -198,7 +268,7 @@ QDebug operator<<(QDebug debug, const QGrpcCallOptions &callOpts)
const QDebugStateSaver save(debug); const QDebugStateSaver save(debug);
debug.nospace().noquote(); debug.nospace().noquote();
debug << "QGrpcCallOptions(deadline: " << callOpts.deadlineTimeout() debug << "QGrpcCallOptions(deadline: " << callOpts.deadlineTimeout()
<< ", metadata: " << callOpts.metadata() << ')'; << ", metadata: " << callOpts.metadata(QtGrpc::MultiValue) << ')';
return debug; return debug;
} }
#endif #endif

View File

@ -5,11 +5,13 @@
#define QGRPCALLOPTIONS_H #define QGRPCALLOPTIONS_H
#include <QtGrpc/qtgrpcglobal.h> #include <QtGrpc/qtgrpcglobal.h>
#include <QtGrpc/qtgrpcnamespace.h>
#include <QtCore/qhash.h> #include <QtCore/qhash.h>
#include <QtCore/qshareddata.h> #include <QtCore/qshareddata.h>
#include <QtCore/qstringfwd.h> #include <QtCore/qstringfwd.h>
#include <QtCore/qtclasshelpermacros.h> #include <QtCore/qtclasshelpermacros.h>
#include <QtCore/qtdeprecationdefinitions.h>
#include <chrono> #include <chrono>
#include <optional> #include <optional>
@ -42,10 +44,24 @@ public:
deadlineTimeout() const noexcept; deadlineTimeout() const noexcept;
Q_GRPC_EXPORT QGrpcCallOptions &setDeadlineTimeout(std::chrono::milliseconds timeout); 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<QByteArray, QByteArray> &metadata() const & noexcept; [[nodiscard]] Q_GRPC_EXPORT const QHash<QByteArray, QByteArray> &metadata() const & noexcept;
QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash")
[[nodiscard]] Q_GRPC_EXPORT QHash<QByteArray, QByteArray> metadata() &&; [[nodiscard]] Q_GRPC_EXPORT QHash<QByteArray, QByteArray> metadata() &&;
QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload")
Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QHash<QByteArray, QByteArray> &metadata); Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QHash<QByteArray, QByteArray> &metadata);
QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload")
Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QHash<QByteArray, QByteArray> &&metadata); Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QHash<QByteArray, QByteArray> &&metadata);
#endif
[[nodiscard]] Q_GRPC_EXPORT const QMultiHash<QByteArray, QByteArray> &
metadata(QtGrpc::MultiValueTag) const & noexcept;
[[nodiscard]] Q_GRPC_EXPORT QMultiHash<QByteArray, QByteArray>
metadata(QtGrpc::MultiValueTag) &&;
Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(const QMultiHash<QByteArray, QByteArray> &metadata);
Q_GRPC_EXPORT QGrpcCallOptions &setMetadata(QMultiHash<QByteArray, QByteArray> &&metadata);
Q_GRPC_EXPORT QGrpcCallOptions &
setMetadata(std::initializer_list<std::pair<QByteArray, QByteArray>> list);
private: private:
QExplicitlySharedDataPointer<QGrpcCallOptionsPrivate> d_ptr; QExplicitlySharedDataPointer<QGrpcCallOptionsPrivate> d_ptr;

View File

@ -1,6 +1,7 @@
// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGrpc/private/qgrpccommonoptions_p.h>
#include <QtGrpc/qgrpcchanneloptions.h> #include <QtGrpc/qgrpcchanneloptions.h>
#include <QtGrpc/qgrpcserializationformat.h> #include <QtGrpc/qgrpcserializationformat.h>
#include <QtGrpc/qtgrpcnamespace.h> #include <QtGrpc/qtgrpcnamespace.h>
@ -25,17 +26,34 @@ using namespace QtGrpc;
to all remote procedure calls (RPCs) that operate on the associated to all remote procedure calls (RPCs) that operate on the associated
channel, which is used to communicate with services. 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 \note It is up to the channel's implementation to determine the specifics
of these options. of these options.
*/ */
class QGrpcChannelOptionsPrivate : public QSharedData class QGrpcChannelOptionsPrivate : public QGrpcCommonOptions
{ {
public: public:
std::optional<std::chrono::milliseconds> timeout;
QHash<QByteArray, QByteArray> metadata;
QGrpcSerializationFormat serializationFormat; QGrpcSerializationFormat serializationFormat;
#if QT_CONFIG(ssl) #if QT_CONFIG(ssl)
std::optional<QSslConfiguration> sslConfiguration; std::optional<QSslConfiguration> sslConfiguration;
@ -101,57 +119,151 @@ QGrpcChannelOptions::operator QVariant() const
} }
/*! /*!
Sets the \a timeout for the channel and returns a reference to the updated \include qgrpccommonoptions.cpp set-deadline-timeout
object.
\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 \sa deadlineTimeout()
operate on the channel, except those overridden by
QGrpcCallOptions::setDeadline().
*/ */
QGrpcChannelOptions &QGrpcChannelOptions::setDeadlineTimeout(std::chrono::milliseconds timeout) QGrpcChannelOptions &QGrpcChannelOptions::setDeadlineTimeout(std::chrono::milliseconds timeout)
{ {
if (d_ptr->timeout == timeout) if (d_ptr->deadlineTimeout() == timeout)
return *this; return *this;
d_ptr.detach(); d_ptr.detach();
Q_D(QGrpcChannelOptions); Q_D(QGrpcChannelOptions);
d->timeout = timeout; d->setDeadlineTimeout(timeout);
return *this; return *this;
} }
#if QT_DEPRECATED_SINCE(6, 13)
/*!
\fn const QHash<QByteArray, QByteArray> &QGrpcChannelOptions::metadata() const &
\fn QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> &QGrpcChannelOptions::metadata() const & noexcept
{
Q_D(const QGrpcChannelOptions);
return d->metadata();
}
QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> &metadata) \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash<QByteArray, QByteArray> &metadata)
\fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata) \fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata)
\deprecated [6.13] Use the QMultiHash overload instead.
Sets the client \a metadata for the channel and returns a reference to the \include qgrpccommonoptions.cpp set-metadata
updated object.
\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<QByteArray,
QByteArray>&)}{QGrpcCallOptions::setMetadata(QMultiHash)}
\note The metadata set via the channel options applies to all RPCs that \sa metadata()
operate on the channel, except those overridden by
QGrpcCallOptions::setMetadata().
*/ */
QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash<QByteArray, QByteArray> &metadata) QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(const QHash<QByteArray, QByteArray> &metadata)
{ {
if (d_ptr->metadata == metadata) if (d_ptr->metadata(QtGrpc::MultiValue) == metadata)
return *this; return *this;
d_ptr.detach(); d_ptr.detach();
Q_D(QGrpcChannelOptions); Q_D(QGrpcChannelOptions);
d->metadata = metadata; d->setMetadata(metadata);
return *this;
}
QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata)
{
if (d_ptr->metadata(QtGrpc::MultiValue) == metadata)
return *this;
d_ptr.detach();
Q_D(QGrpcChannelOptions);
d->setMetadata(std::move(metadata));
return *this; return *this;
} }
QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QHash<QByteArray, QByteArray> &&metadata) #endif // QT_DEPRECATED_SINCE(6, 13)
/*!
\since 6.10
\fn const QMultiHash<QByteArray, QByteArray> &QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag) const &
\fn QMultiHash<QByteArray, QByteArray> QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag) &&
\include qgrpccommonoptions.cpp metadata-multi
\sa {setMetadata(const QMultiHash<QByteArray, QByteArray>&)}{setMetadata}
*/
const QMultiHash<QByteArray, QByteArray> &
QGrpcChannelOptions::metadata(QtGrpc::MultiValueTag tag) const & noexcept
{ {
if (d_ptr->metadata == metadata) Q_D(const QGrpcChannelOptions);
return d->metadata(tag);
}
QMultiHash<QByteArray, QByteArray>
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<QByteArray, QByteArray> &metadata)
\fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QMultiHash<QByteArray, QByteArray> &&metadata)
\fn QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(std::initializer_list<std::pair<QByteArray, QByteArray>> list)
\include qgrpccommonoptions.cpp set-metadata-multi
\include qgrpcchanneloptions.cpp merge-md-note
\l{QGrpcCallOptions::setMetadata(const QMultiHash<QByteArray,
QByteArray>&)}{QGrpcCallOptions::setMetadata(QMultiHash)}
\sa metadata(QtGrpc::MultiValueTag)
*/
QGrpcChannelOptions &
QGrpcChannelOptions::setMetadata(const QMultiHash<QByteArray, QByteArray> &metadata)
{
if (d_ptr->metadata(QtGrpc::MultiValue) == metadata)
return *this; return *this;
d_ptr.detach(); d_ptr.detach();
Q_D(QGrpcChannelOptions); Q_D(QGrpcChannelOptions);
d->metadata = std::move(metadata); d->setMetadata(metadata);
return *this; return *this;
} }
QGrpcChannelOptions &QGrpcChannelOptions::setMetadata(QMultiHash<QByteArray, QByteArray> &&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<std::pair<QByteArray, QByteArray>> list)
{
return setMetadata(QMultiHash<QByteArray, QByteArray>(list));
}
/*! /*!
\since 6.8 \since 6.8
@ -179,29 +291,7 @@ QGrpcChannelOptions::setSerializationFormat(const QGrpcSerializationFormat &form
std::optional<std::chrono::milliseconds> QGrpcChannelOptions::deadlineTimeout() const noexcept std::optional<std::chrono::milliseconds> QGrpcChannelOptions::deadlineTimeout() const noexcept
{ {
Q_D(const QGrpcChannelOptions); Q_D(const QGrpcChannelOptions);
return d->timeout; return d->deadlineTimeout();
}
/*!
\fn const QHash<QByteArray, QByteArray> &QGrpcChannelOptions::metadata() const &
\fn QHash<QByteArray, QByteArray> QGrpcChannelOptions::metadata() &&
Returns the client metadata for the channel.
If this field is unset, returns empty metadata.
*/
const QHash<QByteArray, QByteArray> &QGrpcChannelOptions::metadata() const & noexcept
{
Q_D(const QGrpcChannelOptions);
return d->metadata;
}
QHash<QByteArray, QByteArray> QGrpcChannelOptions::metadata() &&
{
Q_D(QGrpcChannelOptions);
if (d->ref.loadRelaxed() != 1) // return copy if shared
return { d->metadata };
return std::move(d->metadata);
} }
/*! /*!
@ -258,7 +348,7 @@ QDebug operator<<(QDebug debug, const QGrpcChannelOptions &chOpts)
const QDebugStateSaver save(debug); const QDebugStateSaver save(debug);
debug.nospace().noquote(); debug.nospace().noquote();
debug << "QGrpcChannelOptions(deadline: " << chOpts.deadlineTimeout() debug << "QGrpcChannelOptions(deadline: " << chOpts.deadlineTimeout()
<< ", metadata: " << chOpts.metadata() << ", metadata: " << chOpts.metadata(QtGrpc::MultiValue)
<< ", serializationFormat: " << chOpts.serializationFormat().suffix() << ", serializationFormat: " << chOpts.serializationFormat().suffix()
<< ", sslConfiguration: "; << ", sslConfiguration: ";
# if QT_CONFIG(ssl) # if QT_CONFIG(ssl)

View File

@ -5,6 +5,7 @@
#define QGRPCHANNELOPTIONS_H #define QGRPCHANNELOPTIONS_H
#include <QtGrpc/qtgrpcglobal.h> #include <QtGrpc/qtgrpcglobal.h>
#include <QtGrpc/qtgrpcnamespace.h>
#if QT_CONFIG(ssl) #if QT_CONFIG(ssl)
# include <QtNetwork/qsslconfiguration.h> # include <QtNetwork/qsslconfiguration.h>
@ -14,7 +15,7 @@
#include <QtCore/qshareddata.h> #include <QtCore/qshareddata.h>
#include <QtCore/qstringfwd.h> #include <QtCore/qstringfwd.h>
#include <QtCore/qtclasshelpermacros.h> #include <QtCore/qtclasshelpermacros.h>
#include <QtCore/qurl.h> #include <QtCore/qtdeprecationdefinitions.h>
#include <chrono> #include <chrono>
#include <optional> #include <optional>
@ -48,10 +49,25 @@ public:
deadlineTimeout() const noexcept; deadlineTimeout() const noexcept;
Q_GRPC_EXPORT QGrpcChannelOptions &setDeadlineTimeout(std::chrono::milliseconds timeout); 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<QByteArray, QByteArray> &metadata() const & noexcept; [[nodiscard]] Q_GRPC_EXPORT const QHash<QByteArray, QByteArray> &metadata() const & noexcept;
QT_DEPRECATED_VERSION_X_6_13("Use metadata(QtGrpc::MultiValue) for QMultiHash")
[[nodiscard]] Q_GRPC_EXPORT QHash<QByteArray, QByteArray> metadata() &&; [[nodiscard]] Q_GRPC_EXPORT QHash<QByteArray, QByteArray> metadata() &&;
QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload")
Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(const QHash<QByteArray, QByteArray> &metadata); Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(const QHash<QByteArray, QByteArray> &metadata);
QT_DEPRECATED_VERSION_X_6_13("Use the QMultiHash overload")
Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QHash<QByteArray, QByteArray> &&metadata); Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QHash<QByteArray, QByteArray> &&metadata);
#endif
[[nodiscard]] Q_GRPC_EXPORT const QMultiHash<QByteArray, QByteArray> &
metadata(QtGrpc::MultiValueTag) const & noexcept;
[[nodiscard]] Q_GRPC_EXPORT QMultiHash<QByteArray, QByteArray>
metadata(QtGrpc::MultiValueTag) &&;
Q_GRPC_EXPORT QGrpcChannelOptions &
setMetadata(const QMultiHash<QByteArray, QByteArray> &metadata);
Q_GRPC_EXPORT QGrpcChannelOptions &setMetadata(QMultiHash<QByteArray, QByteArray> &&metadata);
Q_GRPC_EXPORT QGrpcChannelOptions &
setMetadata(std::initializer_list<std::pair<QByteArray, QByteArray>> list);
[[nodiscard]] Q_GRPC_EXPORT QGrpcSerializationFormat serializationFormat() const; [[nodiscard]] Q_GRPC_EXPORT QGrpcSerializationFormat serializationFormat() const;
Q_GRPC_EXPORT QGrpcChannelOptions & Q_GRPC_EXPORT QGrpcChannelOptions &

View File

@ -0,0 +1,140 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtGrpc/private/qgrpccommonoptions_p.h>
QT_BEGIN_NAMESPACE
#if QT_DEPRECATED_SINCE(6, 13)
namespace
{
inline QHash<QByteArray, QByteArray> mergeHash(const QMultiHash<QByteArray, QByteArray> &multiHash)
{
QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> &QGrpcCommonOptions::metadata() const &
{
m_deprecatedQHashRefUsed = true;
if (m_metadataMulti != m_metadata)
m_metadata = mergeHash(m_metadataMulti);
return m_metadata;
}
QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> &md)
{
if (m_deprecatedQHashRefUsed)
m_metadata = md;
m_metadataMulti = QMultiHash<QByteArray, QByteArray>(md);
}
void QGrpcCommonOptions::setMetadata(QHash<QByteArray, QByteArray> &&md)
{
if (m_deprecatedQHashRefUsed)
m_metadata = md;
m_metadataMulti = QMultiHash<QByteArray, QByteArray>(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<std::chrono::milliseconds> 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<QByteArray, QByteArray> &
QGrpcCommonOptions::metadata(QtGrpc::MultiValueTag /*tag*/) const &
{
return m_metadataMulti;
}
QMultiHash<QByteArray, QByteArray> 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<QByteArray, QByteArray> &md)
{
m_metadataMulti = md;
#if QT_DEPRECATED_SINCE(6, 13)
if (m_deprecatedQHashRefUsed)
m_metadata = mergeHash(m_metadataMulti);
#endif
}
void QGrpcCommonOptions::setMetadata(QMultiHash<QByteArray, QByteArray> &&md)
{
m_metadataMulti = std::move(md);
#if QT_DEPRECATED_SINCE(6, 13)
if (m_deprecatedQHashRefUsed)
m_metadata = mergeHash(m_metadataMulti);
#endif
}
QT_END_NAMESPACE

View File

@ -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 <QtGrpc/qtgrpcglobal.h>
#include <QtGrpc/qtgrpcnamespace.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qhash.h>
#include <QtCore/qshareddata.h>
#include <chrono>
#include <optional>
QT_BEGIN_NAMESPACE
class QGrpcCommonOptions : public QSharedData
{
public:
QGrpcCommonOptions() = default;
virtual ~QGrpcCommonOptions() = default;
[[nodiscard]] std::optional<std::chrono::milliseconds> deadlineTimeout() const noexcept;
void setDeadlineTimeout(std::chrono::milliseconds t);
#if QT_DEPRECATED_SINCE(6, 13)
const QHash<QByteArray, QByteArray> &metadata() const &;
QHash<QByteArray, QByteArray> metadata() &&;
void setMetadata(const QHash<QByteArray, QByteArray> &md);
void setMetadata(QHash<QByteArray, QByteArray> &&md);
#endif
const QMultiHash<QByteArray, QByteArray> &metadata(QtGrpc::MultiValueTag /*tag*/) const &;
QMultiHash<QByteArray, QByteArray> metadata(QtGrpc::MultiValueTag /*tag*/) &&;
void setMetadata(const QMultiHash<QByteArray, QByteArray> &md);
void setMetadata(QMultiHash<QByteArray, QByteArray> &&md);
private:
std::optional<std::chrono::milliseconds> m_timeout;
QMultiHash<QByteArray, QByteArray> m_metadataMulti;
#if QT_DEPRECATED_SINCE(6, 13)
mutable QHash<QByteArray, QByteArray> m_metadata;
mutable bool m_deprecatedQHashRefUsed = false;
#endif
};
inline bool operator==(const QMultiHash<QByteArray, QByteArray> &multiHash,
const QHash<QByteArray, QByteArray> &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<QByteArray, QByteArray> &multiHash,
const QHash<QByteArray, QByteArray> &hash)
{
return !(multiHash == hash);
}
QT_END_NAMESPACE
#endif

View File

@ -553,8 +553,8 @@ void Http2Handler::prepareInitialRequest(QGrpcOperationContext *operationContext
} }
}; };
iterateMetadata(channelOptions.metadata()); iterateMetadata(channelOptions.metadata(QtGrpc::MultiValue));
iterateMetadata(operationContext->callOptions().metadata()); iterateMetadata(operationContext->callOptions().metadata(QtGrpc::MultiValue));
writeMessage(operationContext->argument()); writeMessage(operationContext->argument());
} }
@ -688,8 +688,8 @@ QGrpcHttp2ChannelPrivate::QGrpcHttp2ChannelPrivate(const QUrl &uri, QGrpcHttp2Ch
: defaultContentType; : defaultContentType;
bool warnAboutFormatConflict = !formatSuffix.isEmpty(); bool warnAboutFormatConflict = !formatSuffix.isEmpty();
const auto it = channelOptions.metadata().constFind(ContentTypeHeader.data()); const auto it = channelOptions.metadata(QtGrpc::MultiValue).constFind(ContentTypeHeader.data());
if (it != channelOptions.metadata().cend()) { if (it != channelOptions.metadata(QtGrpc::MultiValue).cend()) {
if (formatSuffix.isEmpty() && it.value() != DefaultContentType) { if (formatSuffix.isEmpty() && it.value() != DefaultContentType) {
if (it.value() == "application/grpc+json") { if (it.value() == "application/grpc+json") {
channelOptions.setSerializationFormat(SerializationFormat::Json); channelOptions.setSerializationFormat(SerializationFormat::Json);

View File

@ -47,6 +47,14 @@ enum class StatusCode : quint8 {
}; };
Q_ENUM_NS(StatusCode) 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") Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
} // namespace QtGrpc } // namespace QtGrpc

View File

@ -82,3 +82,21 @@
\sa{https://github.com/grpc/grpc/blob/master/doc/statuscodes.md}{gRPC status codes} \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)
*/

View File

@ -37,7 +37,7 @@ public:
explicit QQmlGrpcMetadata(QObject *parent = nullptr); explicit QQmlGrpcMetadata(QObject *parent = nullptr);
~QQmlGrpcMetadata() override; ~QQmlGrpcMetadata() override;
const QHash<QByteArray, QByteArray> &metadata() const & noexcept { return m_metadata; } const QMultiHash<QByteArray, QByteArray> &metadata() const & noexcept { return m_metadata; }
void metadata() && = delete; void metadata() && = delete;
const QVariantMap &data() const { return m_variantdata; } const QVariantMap &data() const { return m_variantdata; }
@ -48,7 +48,7 @@ Q_SIGNALS:
private: private:
QVariantMap m_variantdata; QVariantMap m_variantdata;
QHash<QByteArray, QByteArray> m_metadata; QMultiHash<QByteArray, QByteArray> m_metadata;
Q_DISABLE_COPY_MOVE(QQmlGrpcMetadata) Q_DISABLE_COPY_MOVE(QQmlGrpcMetadata)
}; };

View File

@ -10,6 +10,8 @@ endif()
qt_internal_add_test(tst_qgrpccalloptions qt_internal_add_test(tst_qgrpccalloptions
SOURCES SOURCES
tst_qgrpccalloptions.cpp tst_qgrpccalloptions.cpp
INCLUDE_DIRECTORIES
"${CMAKE_CURRENT_LIST_DIR}/../shared"
LIBRARIES LIBRARIES
Qt::Core Qt::Core
Qt::Test Qt::Test

View File

@ -1,129 +1,32 @@
// Copyright (C) 2024 The Qt Company Ltd. // Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <grpccommonoptions.h>
#include <QtGrpc/qgrpccalloptions.h> #include <QtGrpc/qgrpccalloptions.h>
#include <QtTest/qtest.h> #include <QtTest/qtest.h>
#include <cstring>
using namespace std::chrono_literals;
class QGrpcCallOptionsTest : public QObject class QGrpcCallOptionsTest : public QObject
{ {
Q_OBJECT Q_OBJECT
private Q_SLOTS: private Q_SLOTS:
void hasSpecialMemberFunctions() const; void hasSpecialMemberFunctions() const { common.hasSpecialMemberFunctions(); }
void hasImplicitQVariant() const; void hasImplicitQVariant() const { common.hasImplicitQVariant(); }
void hasMemberSwap() const; void hasMemberSwap() const { common.hasMemberSwap(); }
void propertyMetadata() const; #if QT_DEPRECATED_SINCE(6, 13)
void propertyDeadline() const; void deprecatedPropertyMetadata() const { common.deprecatedPropertyMetadata(); }
void streamsToDebug() const; void propertyMetadataCompat() const { common.propertyMetadataCompat(); }
#endif
void propertyMetadata() const { common.propertyMetadata(); }
void propertyDeadline() const { common.propertyDeadline(); }
void streamsToDebug() const { common.streamsToDebug(); }
private:
GrpcCommonOptionsTest<QGrpcCallOptions> 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<QGrpcCallOptions>());
const auto o2 = v.value<QGrpcCallOptions>();
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<QByteArray, QByteArray> 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<char[]> ustr(QTest::toString(o));
QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get()));
}
QTEST_MAIN(QGrpcCallOptionsTest) QTEST_MAIN(QGrpcCallOptionsTest)
#include "tst_qgrpccalloptions.moc" #include "tst_qgrpccalloptions.moc"

View File

@ -10,6 +10,8 @@ endif()
qt_internal_add_test(tst_qgrpcchanneloptions qt_internal_add_test(tst_qgrpcchanneloptions
SOURCES SOURCES
tst_qgrpcchanneloptions.cpp tst_qgrpcchanneloptions.cpp
INCLUDE_DIRECTORIES
"${CMAKE_CURRENT_LIST_DIR}/../shared"
LIBRARIES LIBRARIES
Qt::Core Qt::Core
Qt::Test Qt::Test

View File

@ -1,6 +1,8 @@
// Copyright (C) 2024 The Qt Company Ltd. // Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <grpccommonoptions.h>
#include <QtGrpc/qgrpcchanneloptions.h> #include <QtGrpc/qgrpcchanneloptions.h>
#include <QtGrpc/qgrpcserializationformat.h> #include <QtGrpc/qgrpcserializationformat.h>
@ -8,116 +10,31 @@
#include <cstring> #include <cstring>
using namespace std::chrono_literals;
class QGrpcChannelOptionsTest : public QObject class QGrpcChannelOptionsTest : public QObject
{ {
Q_OBJECT Q_OBJECT
private Q_SLOTS: private Q_SLOTS:
void hasSpecialMemberFunctions() const; void hasSpecialMemberFunctions() const { common.hasSpecialMemberFunctions(); }
void hasImplicitQVariant() const; void hasImplicitQVariant() const { common.hasImplicitQVariant(); }
void hasMemberSwap() const; void hasMemberSwap() const { common.hasMemberSwap(); }
void propertyMetadata() const; #if QT_DEPRECATED_SINCE(6, 13)
void propertyDeadline() const; 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; void propertySerializationFormat() const;
#if QT_CONFIG(ssl) #if QT_CONFIG(ssl)
void propertySslConfiguration() const; void propertySslConfiguration() const;
#endif #endif
void streamsToDebug() const;
private:
GrpcCommonOptionsTest<QGrpcChannelOptions> 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<QGrpcChannelOptions>());
const auto o2 = v.value<QGrpcChannelOptions>();
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<QByteArray, QByteArray> 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 void QGrpcChannelOptionsTest::propertySerializationFormat() const
{ {
QGrpcSerializationFormat fmt(QtGrpc::SerializationFormat::Json); QGrpcSerializationFormat fmt(QtGrpc::SerializationFormat::Json);
@ -147,20 +64,6 @@ void QGrpcChannelOptionsTest::propertySslConfiguration() const
} }
#endif #endif
void QGrpcChannelOptionsTest::streamsToDebug() const
{
QGrpcChannelOptions o;
QString storage;
QDebug dbg(&storage);
dbg.nospace().noquote();
dbg << o;
QVERIFY(!storage.isEmpty());
std::unique_ptr<char[]> ustr(QTest::toString(o));
QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get()));
}
QTEST_MAIN(QGrpcChannelOptionsTest) QTEST_MAIN(QGrpcChannelOptionsTest)
#include "tst_qgrpcchanneloptions.moc" #include "tst_qgrpcchanneloptions.moc"

View File

@ -0,0 +1,237 @@
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/qtest.h>
#include <QtGrpc/qtgrpcnamespace.h>
#include <QtCore/qdebug.h>
#include <QtCore/qstring.h>
#include <QtCore/qhash.h>
using namespace std::chrono_literals;
template <typename T>
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<T>());
const auto o2 = v.value<T>();
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<QByteArray, QByteArray> 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<QByteArray, QByteArray> &m) {
return QMultiHash<QByteArray, QByteArray>(m);
};
QMultiHash<QByteArray, QByteArray> multiMd = {
{ "keyA", "valA1" },
{ "keyA", "valA2" },
{ "keyB", "valB1" },
{ "keyB", "valB2" },
{ "keyC", "valC" },
};
QHash<QByteArray, QByteArray> 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<QByteArray, QByteArray>{
{"keyA","valA"}, {"keyB","valB"}
});
// o3aMd is not affected by the update since o3B deprecatedQHashRef is used
QCOMPARE_NE(o3aMd, o3B.metadata());
o3A.setMetadata(QHash<QByteArray, QByteArray>{
{"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<std::pair<QByteArray, QByteArray>> list = {
{ "keyA", "valA1" },
{ "keyA", "valA2" },
{ "keyB", "valB" },
};
QMultiHash<QByteArray, QByteArray> 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<char[]> ustr(QTest::toString(o));
QCOMPARE_EQ(storage, QString::fromUtf8(ustr.get()));
}
};