From f610826ef0a5cfbd31cdaf4e4d56e0ad4b26b7ae Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Mon, 21 May 2018 21:33:05 +0200 Subject: [PATCH] Implement QtPromise::attempt(functor, args...) Add a new helper that calls functor immediately and returns a promise fulfilled with the value returned by functor. Any synchronous exceptions will be turned into rejections on the returned promise. This is a convenient method that can be used instead of handling both synchronous and asynchronous exception flows. Also simplify PromiseDispatch which now calls the functor with a variable number of arguments (including none). --- docs/SUMMARY.md | 1 + docs/qtpromise/api-reference.md | 1 + docs/qtpromise/helpers/attempt.md | 41 ++++++++ src/qtpromise/qpromise_p.h | 81 +++++---------- src/qtpromise/qpromisehelpers.h | 26 +++++ .../qtpromise/helpers/attempt/attempt.pro | 5 + .../qtpromise/helpers/attempt/tst_attempt.cpp | 99 +++++++++++++++++++ tests/auto/qtpromise/helpers/helpers.pro | 1 + 8 files changed, 200 insertions(+), 55 deletions(-) create mode 100644 docs/qtpromise/helpers/attempt.md create mode 100644 tests/auto/qtpromise/helpers/attempt/attempt.pro create mode 100644 tests/auto/qtpromise/helpers/attempt/tst_attempt.cpp diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 3086aef..8f43121 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -22,5 +22,6 @@ * [::resolve (static)](qtpromise/qpromise/resolve.md) * [qPromise](qtpromise/helpers/qpromise.md) * [qPromiseAll](qtpromise/helpers/qpromiseall.md) + * [QtPromise::attempt](qtpromise/helpers/attempt.md) * [QtPromise::filter](qtpromise/helpers/filter.md) * [QtPromise::map](qtpromise/helpers/map.md) diff --git a/docs/qtpromise/api-reference.md b/docs/qtpromise/api-reference.md index e32d3db..ee84e52 100644 --- a/docs/qtpromise/api-reference.md +++ b/docs/qtpromise/api-reference.md @@ -27,5 +27,6 @@ * [`qPromise`](helpers/qpromise.md) * [`qPromiseAll`](helpers/qpromiseall.md) +* [`QtPromise::attempt`](helpers/attempt.md) * [`QtPromise::filter`](helpers/filter.md) * [`QtPromise::map`](helpers/map.md) diff --git a/docs/qtpromise/helpers/attempt.md b/docs/qtpromise/helpers/attempt.md new file mode 100644 index 0000000..a2908bf --- /dev/null +++ b/docs/qtpromise/helpers/attempt.md @@ -0,0 +1,41 @@ +## `QtPromise::attempt` + +```cpp +QtPromise::attempt(Functor functor, Args...) -> QPromise + +// With: +// - Functor: Function(Args...) -> R | QPromise +``` + +Calls `functor` immediately and returns a promise fulfilled with the value returned by +`functor`. Any synchronous exceptions will be turned into rejections on the returned +promise. This is a convenient method that can be used instead of handling both synchronous +and asynchronous exception flows. + +The type `R` of the `output` promise depends on the type returned by the `functor` function. +If `functor` returns a promise (or `QFuture`), the `output` promise is delayed and will be +resolved by the returned promise. + +```cpp +QPromise download(const QUrl& url); + +QPromise process(const QUrl& url) +{ + return QtPromise::attempt([&]() { + if (!url.isValid()) { + throw InvalidUrlException(); + } + + return download(url); + } +} + +auto output = process(url); + +// 'output' type: QPromise +output.then([](const QByteArray& res) { + // {...} +}).fail([](const InvalidUrlException& err) { + // {...} +}); +``` diff --git a/src/qtpromise/qpromise_p.h b/src/qtpromise/qpromise_p.h index f45695f..5273e30 100644 --- a/src/qtpromise/qpromise_p.h +++ b/src/qtpromise/qpromise_p.h @@ -119,6 +119,13 @@ struct PromiseDeduce> : public PromiseDeduce { }; +template +struct PromiseFunctor +{ + using ResultType = typename std::result_of::type; + using PromiseType = typename PromiseDeduce::Type; +}; + template struct PromiseFulfill { @@ -176,51 +183,17 @@ struct PromiseFulfill> } }; -template +template struct PromiseDispatch { - using Promise = typename PromiseDeduce::Type; - using ResType = Unqualified; - - template - static void call(const T& value, THandler handler, const TResolve& resolve, const TReject& reject) + template + static void call(const Resolve& resolve, const Reject& reject, Functor fn, Args&&... args) { try { - PromiseFulfill::call(handler(value), resolve, reject); - } catch (...) { - reject(std::current_exception()); - } - } -}; - -template -struct PromiseDispatch -{ - using Promise = QtPromise::QPromise; - - template - static void call(const T& value, THandler handler, const TResolve& resolve, const TReject& reject) - { - try { - handler(value); - resolve(); - } catch (...) { - reject(std::current_exception()); - } - } -}; - -template -struct PromiseDispatch -{ - using Promise = typename PromiseDeduce::Type; - using ResType = Unqualified; - - template - static void call(THandler handler, const TResolve& resolve, const TReject& reject) - { - try { - PromiseFulfill::call(handler(), resolve, reject); + PromiseFulfill>::call( + fn(std::forward(args)...), + resolve, + reject); } catch (...) { reject(std::current_exception()); } @@ -228,15 +201,13 @@ struct PromiseDispatch }; template <> -struct PromiseDispatch +struct PromiseDispatch { - using Promise = QtPromise::QPromise; - - template - static void call(THandler handler, const TResolve& resolve, const TReject& reject) + template + static void call(const Resolve& resolve, const Reject& reject, Functor fn, Args&&... args) { try { - handler(); + fn(std::forward(args)...); resolve(); } catch (...) { reject(std::current_exception()); @@ -248,7 +219,7 @@ template ::type; - using Promise = typename PromiseDispatch::Promise; + using Promise = typename PromiseDeduce::Type; template static std::function create( @@ -257,7 +228,7 @@ struct PromiseHandler const TReject& reject) { return [=](const T& value) { - PromiseDispatch::call(value, std::move(handler), resolve, reject); + PromiseDispatch::call(resolve, reject, handler, value); }; } }; @@ -266,7 +237,7 @@ template struct PromiseHandler { using ResType = typename std::result_of::type; - using Promise = typename PromiseDispatch::Promise; + using Promise = typename PromiseDeduce::Type; template static std::function create( @@ -275,7 +246,7 @@ struct PromiseHandler const TReject& reject) { return [=](const T&) { - PromiseDispatch::call(handler, resolve, reject); + PromiseDispatch::call(resolve, reject, handler); }; } }; @@ -284,7 +255,7 @@ template struct PromiseHandler { using ResType = typename std::result_of::type; - using Promise = typename PromiseDispatch::Promise; + using Promise = typename PromiseDeduce::Type; template static std::function create( @@ -293,7 +264,7 @@ struct PromiseHandler const TReject& reject) { return [=]() { - PromiseDispatch::call(handler, resolve, reject); + PromiseDispatch::call(resolve, reject, handler); }; } }; @@ -351,7 +322,7 @@ struct PromiseCatcher try { error.rethrow(); } catch (const TArg& error) { - PromiseDispatch::call(error, handler, resolve, reject); + PromiseDispatch::call(resolve, reject, handler, error); } catch (...) { reject(std::current_exception()); } @@ -374,7 +345,7 @@ struct PromiseCatcher try { error.rethrow(); } catch (...) { - PromiseDispatch::call(handler, resolve, reject); + PromiseDispatch::call(resolve, reject, handler); } }; } diff --git a/src/qtpromise/qpromisehelpers.h b/src/qtpromise/qpromisehelpers.h index 463b2f1..0b08903 100644 --- a/src/qtpromise/qpromisehelpers.h +++ b/src/qtpromise/qpromisehelpers.h @@ -38,6 +38,32 @@ static inline QPromise qPromiseAll(const Sequence, Args...> return QPromise::all(promises); } +template +static inline typename QtPromisePrivate::PromiseFunctor::PromiseType +attempt(Functor&& fn, Args&&... args) +{ + using namespace QtPromisePrivate; + using FunctorType = PromiseFunctor; + using PromiseType = typename FunctorType::PromiseType; + using ValueType = typename PromiseType::Type; + + // NOTE: std::forward>: MSVC 2013 fails when forwarding + // template type (error: "expects 4 arguments - 0 provided"). + // However it succeeds with type alias. + // TODO: should we expose QPromise::ResolveType & RejectType? + using ResolveType = QPromiseResolve; + using RejectType = QPromiseReject; + + return PromiseType( + [&](ResolveType&& resolve, RejectType&& reject) { + PromiseDispatch::call( + std::forward(resolve), + std::forward(reject), + std::forward(fn), + std::forward(args)...); + }); +} + template static inline typename QtPromisePrivate::PromiseMapper::PromiseType map(const Sequence& values, Functor fn) diff --git a/tests/auto/qtpromise/helpers/attempt/attempt.pro b/tests/auto/qtpromise/helpers/attempt/attempt.pro new file mode 100644 index 0000000..b7c9ac8 --- /dev/null +++ b/tests/auto/qtpromise/helpers/attempt/attempt.pro @@ -0,0 +1,5 @@ +QT += concurrent +TARGET = tst_helpers_attempt +SOURCES += $$PWD/tst_attempt.cpp + +include(../../qtpromise.pri) diff --git a/tests/auto/qtpromise/helpers/attempt/tst_attempt.cpp b/tests/auto/qtpromise/helpers/attempt/tst_attempt.cpp new file mode 100644 index 0000000..5eba854 --- /dev/null +++ b/tests/auto/qtpromise/helpers/attempt/tst_attempt.cpp @@ -0,0 +1,99 @@ +// Tests +#include "../../shared/utils.h" + +// QtPromise +#include + +// Qt +#include +#include + +// STL +#include + +using namespace QtPromise; + +class tst_helpers_attempt : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void voidResult(); + void typedResult(); + void futureResult(); + void promiseResult(); + void functorThrows(); + void callWithParams(); +}; + +QTEST_MAIN(tst_helpers_attempt) +#include "tst_attempt.moc" + +void tst_helpers_attempt::voidResult() +{ + auto p = QtPromise::attempt([]() {}); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isFulfilled(), true); + QCOMPARE(waitForValue(p, -1, 42), 42); +} + +void tst_helpers_attempt::typedResult() +{ + auto p = QtPromise::attempt([]() { + return QString("foo"); + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isFulfilled(), true); + QCOMPARE(waitForValue(p, QString()), QString("foo")); +} + +void tst_helpers_attempt::futureResult() +{ + auto p = QtPromise::attempt([]() { + return QtConcurrent::run([]() { + return QString("foo"); + }); + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForValue(p, QString()), QString("foo")); +} + +void tst_helpers_attempt::promiseResult() +{ + auto p = QtPromise::attempt([]() { + return QtPromise::qPromise(42).delay(200); + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isPending(), true); + QCOMPARE(waitForValue(p, -1), 42); +} + +void tst_helpers_attempt::functorThrows() +{ + auto p = QtPromise::attempt([]() { + if (true) { + throw QString("bar"); + } + return 42; + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isRejected(), true); + QCOMPARE(waitForError(p, QString()), QString("bar")); +} + +void tst_helpers_attempt::callWithParams() +{ + auto p = QtPromise::attempt([&](int i, const QString& s) { + return QString("%1:%2").arg(i).arg(s); + }, 42, "foo"); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(p.isFulfilled(), true); + QCOMPARE(waitForValue(p, QString()), QString("42:foo")); +} diff --git a/tests/auto/qtpromise/helpers/helpers.pro b/tests/auto/qtpromise/helpers/helpers.pro index 5e340f1..e40e04b 100644 --- a/tests/auto/qtpromise/helpers/helpers.pro +++ b/tests/auto/qtpromise/helpers/helpers.pro @@ -1,6 +1,7 @@ TEMPLATE = subdirs SUBDIRS += \ all \ + attempt \ filter \ map \ reject \