From 0c3955cca58580af12dff999143e8376f5a4c935 Mon Sep 17 00:00:00 2001 From: Dmitriy Purgin Date: Sun, 22 Nov 2020 17:26:06 +0100 Subject: [PATCH] Implement QPromise::convert() (#41) Converts the resolved value of `QPromise` to the type `U`. Depending on types `T` and `U`, it performs a static cast, calls a converting constructor or tries to convert using `QVariant`. --- docs/.vuepress/config.js | 2 + docs/qtpromise/api-reference.md | 2 + docs/qtpromise/exceptions/conversion.md | 13 + docs/qtpromise/qpromise/convert.md | 124 ++++++++ src/qtpromise/qpromise.h | 3 + src/qtpromise/qpromise.inl | 7 + src/qtpromise/qpromise_p.h | 60 ++++ src/qtpromise/qpromiseexceptions.h | 10 + .../qtpromise/exceptions/tst_exceptions.cpp | 6 + tests/auto/qtpromise/qpromise/CMakeLists.txt | 1 + tests/auto/qtpromise/qpromise/tst_convert.cpp | 266 ++++++++++++++++++ 11 files changed, 494 insertions(+) create mode 100644 docs/qtpromise/exceptions/conversion.md create mode 100644 docs/qtpromise/qpromise/convert.md create mode 100644 tests/auto/qtpromise/qpromise/tst_convert.cpp diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 0ad7bc3..f39f9f4 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -40,6 +40,7 @@ module.exports = { title: 'QPromise', children: [ '/qtpromise/qpromise/constructor', + '/qtpromise/qpromise/convert', '/qtpromise/qpromise/delay', '/qtpromise/qpromise/each', '/qtpromise/qpromise/fail', @@ -77,6 +78,7 @@ module.exports = { children: [ '/qtpromise/exceptions/canceled', '/qtpromise/exceptions/context', + '/qtpromise/exceptions/conversion', '/qtpromise/exceptions/timeout', '/qtpromise/exceptions/undefined' ] diff --git a/docs/qtpromise/api-reference.md b/docs/qtpromise/api-reference.md index 5046184..c2dd1c8 100644 --- a/docs/qtpromise/api-reference.md +++ b/docs/qtpromise/api-reference.md @@ -3,6 +3,7 @@ ## Functions - [`QPromise::QPromise`](qpromise/constructor.md) +- [`QPromise::convert`](qpromise/convert.md) - [`QPromise::delay`](qpromise/delay.md) - [`QPromise::each`](qpromise/each.md) - [`QPromise::fail`](qpromise/fail.md) @@ -39,6 +40,7 @@ - [`QPromiseCanceledException`](exceptions/canceled.md) - [`QPromiseContextException`](exceptions/context.md) +- [`QPromiseConversionException`](exceptions/conversion.md) - [`QPromiseTimeoutException`](exceptions/timeout.md) - [`QPromiseUndefinedException`](exceptions/undefined.md) diff --git a/docs/qtpromise/exceptions/conversion.md b/docs/qtpromise/exceptions/conversion.md new file mode 100644 index 0000000..0c15a1e --- /dev/null +++ b/docs/qtpromise/exceptions/conversion.md @@ -0,0 +1,13 @@ +# QPromiseConversionException + +*Since: 0.7.0* + +This exception is thrown whenever a promise result conversion fails, for example: + +```cpp +QPromise input = {...}; +auto output = input.convert() + .fail([](const QPromiseconversionException& e) { + // conversion may fail because input could not be converted to number + }); +``` diff --git a/docs/qtpromise/qpromise/convert.md b/docs/qtpromise/qpromise/convert.md new file mode 100644 index 0000000..f9ab01a --- /dev/null +++ b/docs/qtpromise/qpromise/convert.md @@ -0,0 +1,124 @@ +--- +title: .convert +--- + +# QPromise::convert + +*Since: 0.7.0* + +```cpp +QPromise::convert() -> QPromise +``` + +This method converts the resolved value of `QPromise` to the type `U`. Depending on types `T` +and `U`, it performs a [static cast](https://en.cppreference.com/w/cpp/language/static_cast), +calls a [converting constructor](https://en.cppreference.com/w/cpp/language/converting_constructor), +or tries to convert using [QVariant](https://doc.qt.io/qt-5/qvariant.html). + +If `T` and `U` are [fundamental types](https://en.cppreference.com/w/cpp/language/types) or +[enumerations](https://en.cppreference.com/w/cpp/language/enum), the result of the conversion is +the same as calling `static_cast` for type `T`: + +```cpp +QPromise input = {...} + +// output type: QPromise +auto output = input.convert(); + +output.then([](double value) { + // the value has been converted using static_cast +}); +``` + +If `U` has a [converting constructor](https://en.cppreference.com/w/cpp/language/converting_constructor) +from `T`, i.e., a non-explicit constructor with a single argument accepting `T`, it is used to +convert the value: + +```cpp +QPromise input = {...} + +// output type: QPromise +auto output = input.convert(); + +output.then([](const QString& value) { + // the value has been converted using static_cast that effectively calls QString(QByteArray) +}); +``` + +::: tip NOTE +When using this method to convert to your own classes, make sure that the constructor meeting the +converting constructor criteria actually performs conversion. +::: + +::: tip NOTE +If `U` is `void`, the resolved value of `QPromise` is dropped. +::: + +Calling this method for `QPromise` tries to convert the resolved `QVariant` to type `U` +using the `QVariant` [conversion algorithm](https://doc.qt.io/qt-5/qvariant.html#using-canconvert-and-convert-consecutively). +For example, this allows to convert a string contained in `QVariant` to number. If such a +conversion fails, the promise is rejected with +[`QPromiseConversionException`](../exceptions/conversion.md). + +```cpp +// resolves to QVariant(int, 42) or QVariant(string, "foo") +QPromise input = {...}; + +auto output = input.convert(); + +// output type: QPromise +output.then([](int value) { + // input was QVariant(int, 42), value is 42 +}) +.fail(const QPromiseConversionException& e) { + // input was QVariant(string, "foo") +}); +``` + +Conversion of `T` to `QVariant` using this method effectively calls `QVariant::fromValue()`. +All custom types must be registered with +[`Q_DECLARE_METATYPE`](https://doc.qt.io/qt-5/qmetatype.html#Q_DECLARE_METATYPE) for this +conversion to work: + +```cpp +struct Foo {}; +Q_DECLARE_METATYPE(Foo); + +QPromise input = {...} + +auto output = input.convert(); + +// output type: QPromise +output.then([](const QVariant& value) { + // value contains an instance of Foo +}); +``` + +All other combinations of `T` and `U` are converted via `QVariant`. All non-Qt types should provide +a [conversion function](https://doc.qt.io/qt-5/qmetatype.html#registerConverter), otherwise the +promise is rejected with [`QPromiseConversionException`](../exceptions/conversion.md): + +```cpp +struct Foo {}; +Q_DECLARE_METATYPE(Foo); + +QMetaType::registerConverter([](const Foo& foo) { + return QString{...}; +}); + +QPromise input = {...} + +auto output = input.convert(); + +// output type: QPromise +output.then([](const QString& value) { + // value contains a result produced by the custom converter +}) +.fail([](const QPromiseConversionException& e) { + // QVariant was unable to convert Foo to QString +}) +``` + +::: warning IMPORTANT +Calling this method for `QPromise` is not supported. +::: diff --git a/src/qtpromise/qpromise.h b/src/qtpromise/qpromise.h index e5943a5..d24a111 100644 --- a/src/qtpromise/qpromise.h +++ b/src/qtpromise/qpromise.h @@ -111,6 +111,9 @@ public: QPromise(F&& resolver) : QPromiseBase(std::forward(resolver)) { } + template + inline QPromise convert() const; + template inline QPromise each(Functor fn); diff --git a/src/qtpromise/qpromise.inl b/src/qtpromise/qpromise.inl index d293fad..ea8aef3 100644 --- a/src/qtpromise/qpromise.inl +++ b/src/qtpromise/qpromise.inl @@ -242,6 +242,13 @@ inline QPromise> QPromise::all(const Sequence, Args... return QtPromise::all(promises); } +template +template +inline QPromise QPromise::convert() const +{ + return QPromiseBase::then(QtPromisePrivate::PromiseConverter::create()); +} + template inline QPromise QPromise::resolve(const T& value) { diff --git a/src/qtpromise/qpromise_p.h b/src/qtpromise/qpromise_p.h index 33eff9f..87b5cc5 100644 --- a/src/qtpromise/qpromise_p.h +++ b/src/qtpromise/qpromise_p.h @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace QtPromise { @@ -30,6 +31,8 @@ class QPromiseResolve; template class QPromiseReject; +class QPromiseConversionException; + } // namespace QtPromise namespace QtPromisePrivate { @@ -553,6 +556,63 @@ struct PromiseInspect } }; +template +struct PromiseConverterBase; + +template +struct PromiseConverterBase +{ + static std::function create() + { + return [](const T& value) { + return static_cast(value); + }; + } +}; + +template +struct PromiseConverterBase +{ + static std::function create() + { + return [](const T& value) { + auto tmp = QVariant::fromValue(value); + + // https://doc.qt.io/qt-5/qvariant.html#using-canconvert-and-convert-consecutively + if (tmp.canConvert(qMetaTypeId()) && tmp.convert(qMetaTypeId())) { + return qvariant_cast(tmp); + } + + throw QtPromise::QPromiseConversionException{}; + }; + } +}; + +template +struct PromiseConverterBase +{ + static std::function create() + { + return [](const T& value) { + return QVariant::fromValue(value); + }; + } +}; + +template +struct PromiseConverter + : PromiseConverterBase::value || + // Conversion to void. + std::is_same::value || + // Conversion between enums and arithmetic types. + ((std::is_enum::value && std::is_arithmetic::value) + || (std::is_arithmetic::value && std::is_enum::value) + || (std::is_enum::value && std::is_enum::value))> +{ }; + } // namespace QtPromisePrivate #endif // QTPROMISE_QPROMISE_H diff --git a/src/qtpromise/qpromiseexceptions.h b/src/qtpromise/qpromiseexceptions.h index ee58e24..64963bb 100644 --- a/src/qtpromise/qpromiseexceptions.h +++ b/src/qtpromise/qpromiseexceptions.h @@ -35,6 +35,16 @@ public: } }; +class QPromiseConversionException : public QException +{ +public: + void raise() const Q_DECL_OVERRIDE { throw *this; } + QPromiseConversionException* clone() const Q_DECL_OVERRIDE + { + return new QPromiseConversionException{*this}; + } +}; + class QPromiseTimeoutException : public QException { public: diff --git a/tests/auto/qtpromise/exceptions/tst_exceptions.cpp b/tests/auto/qtpromise/exceptions/tst_exceptions.cpp index 5d9a8aa..8250415 100644 --- a/tests/auto/qtpromise/exceptions/tst_exceptions.cpp +++ b/tests/auto/qtpromise/exceptions/tst_exceptions.cpp @@ -20,6 +20,7 @@ class tst_exceptions : public QObject private Q_SLOTS: void canceled(); void context(); + void conversion(); void timeout(); void undefined(); @@ -53,6 +54,11 @@ void tst_exceptions::context() verify(); } +void tst_exceptions::conversion() +{ + verify(); +} + void tst_exceptions::timeout() { verify(); diff --git a/tests/auto/qtpromise/qpromise/CMakeLists.txt b/tests/auto/qtpromise/qpromise/CMakeLists.txt index f30b65e..07a24a0 100644 --- a/tests/auto/qtpromise/qpromise/CMakeLists.txt +++ b/tests/auto/qtpromise/qpromise/CMakeLists.txt @@ -1,6 +1,7 @@ qtpromise_add_tests(qpromise SOURCES tst_construct.cpp + tst_convert.cpp tst_delay.cpp tst_each.cpp tst_fail.cpp diff --git a/tests/auto/qtpromise/qpromise/tst_convert.cpp b/tests/auto/qtpromise/qpromise/tst_convert.cpp new file mode 100644 index 0000000..fecfc59 --- /dev/null +++ b/tests/auto/qtpromise/qpromise/tst_convert.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (c) Simon Brunel, https://github.com/simonbrunel + * + * This source code is licensed under the MIT license found in + * the LICENSE file in the root directory of this source tree. + */ + +#include "../shared/utils.h" + +#include +#include + +#include + +using namespace QtPromise; + +class tst_qpromise_convert : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + + void fulfillTAsU(); + void fulfillTAsVoid(); + void fulfillTAsQVariant(); + void fulfillQVariantAsU(); + void fulfillQVariantAsVoid(); + + void rejectUnconvertibleTypes(); +}; + +QTEST_MAIN(tst_qpromise_convert) +#include "tst_convert.moc" + +namespace { +struct Foo +{ + Foo() = default; + Foo(int foo) : m_foo{foo} { } + + bool operator==(const Foo& rhs) const { return m_foo == rhs.m_foo; } + + int m_foo{-1}; +}; + +struct Bar +{ + Bar() = default; + Bar(const Foo& other) : m_bar{other.m_foo} { } + + bool operator==(const Bar& rhs) const { return m_bar == rhs.m_bar; } + + int m_bar{-1}; +}; + +enum class Enum1 { Value0, Value1, Value2 }; +enum class Enum2 { Value0, Value1, Value2 }; +} // namespace + +Q_DECLARE_METATYPE(Foo) +Q_DECLARE_METATYPE(Bar) + +void tst_qpromise_convert::initTestCase() +{ + // Register converter used by QVariant. + // https://doc.qt.io/qt-5/qmetatype.html#registerConverter + QMetaType::registerConverter([](const Foo& foo) { + return QString{"Foo{%1}"}.arg(foo.m_foo); + }); +} + +void tst_qpromise_convert::fulfillTAsU() +{ + // Static cast between primitive types. + { + auto p = QtPromise::resolve(42.13).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, -1), 42); + QVERIFY(p.isFulfilled()); + } + + // Convert enum class to int. + { + auto p = QtPromise::resolve(Enum1::Value1).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, -1), 1); + QVERIFY(p.isFulfilled()); + } + + // Convert int to enum class. + { + auto p = QtPromise::resolve(1).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, Enum1::Value0), Enum1::Value1); + QVERIFY(p.isFulfilled()); + } + + // Convert between enums + { + auto p = QtPromise::resolve(Enum1::Value1).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, Enum2::Value0), Enum2::Value1); + QVERIFY(p.isFulfilled()); + } + + // Converting constructor for Qt types. + // https://en.cppreference.com/w/cpp/language/converting_constructor + { + auto p = QtPromise::resolve(QByteArray{"foo"}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, QString{}), QString{"foo"}); + QVERIFY(p.isFulfilled()); + } + + // Converting constructor for non-Qt types. + // https://en.cppreference.com/w/cpp/language/converting_constructor + { + auto p = QtPromise::resolve(Foo{42}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, Bar{}), Bar{42}); + QVERIFY(p.isFulfilled()); + } + + // Conversion of types Qt is aware of via QVariant. + { + auto p = QtPromise::resolve(42).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, QString{}), QString{"42"}); + QVERIFY(p.isFulfilled()); + } + + // Conversion of a non-Qt type via QVariant. + // https://doc.qt.io/qt-5/qmetatype.html#registerConverter + { + auto p = QtPromise::resolve(Foo{42}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, QString{}), QString{"Foo{42}"}); + QVERIFY(p.isFulfilled()); + } +} + +void tst_qpromise_convert::fulfillTAsVoid() +{ + auto p = QtPromise::resolve(42).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, -1, 42), 42); + QVERIFY(p.isFulfilled()); +} + +void tst_qpromise_convert::fulfillTAsQVariant() +{ + // Primitive type to QVariant. + { + auto p = QtPromise::resolve(42).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, QVariant{}), QVariant{42}); + QVERIFY(p.isFulfilled()); + } + + // Non-Qt user-defined type to QVariant. + { + auto p = QtPromise::resolve(Foo{42}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QVariant value = waitForValue(p, QVariant{}); + QCOMPARE(value, QVariant::fromValue(Foo{42})); + QCOMPARE(value.value().m_foo, 42); + QVERIFY(p.isFulfilled()); + } +} + +void tst_qpromise_convert::fulfillQVariantAsU() +{ + // Test whether a directly stored value can be extracted. + { + auto p = QtPromise::resolve(QVariant{42}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, -1), 42); + QVERIFY(p.isFulfilled()); + } + + // Test automatic conversion from string performed by QVariant. + // https://doc.qt.io/qt-5/qvariant.html#toInt + { + auto p = QtPromise::resolve(QVariant{"42"}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, -1), 42); + QVERIFY(p.isFulfilled()); + } + + // Non-Qt user-defined type + { + auto p = QtPromise::resolve(QVariant::fromValue(Foo{42})).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, Foo{}), Foo{42}); + QVERIFY(p.isFulfilled()); + } +} + +void tst_qpromise_convert::fulfillQVariantAsVoid() +{ + auto p = QtPromise::resolve(QVariant{42}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QCOMPARE(waitForValue(p, -1, 42), 42); + QVERIFY(p.isFulfilled()); +} + +void tst_qpromise_convert::rejectUnconvertibleTypes() +{ + // A string incompatible with int due to its value. + { + auto p = QtPromise::resolve(QString{"42foo"}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QVERIFY(waitForRejected(p)); + } + + // A user-defined type unconvertible to string because there is no converter. + { + auto p = QtPromise::resolve(QVariant::fromValue(Bar{42})).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QVERIFY(waitForRejected(p)); + } + + // A standard library type unconvertible to a primitive type because there is no converter. + { + auto p = QtPromise::resolve(std::vector{42, -42}).convert(); + + Q_STATIC_ASSERT((std::is_same>::value)); + + QVERIFY(waitForRejected(p)); + } +}