From 051fed5fbc881323bdf87712538c6fc7cf6a4160 Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Fri, 25 May 2018 08:55:20 +0200 Subject: [PATCH] Implement QPromise>::each(functor) Call the given `functor` on each element in the promise value (i.e. `Sequence`), then resolve to the original sequence unmodified. Also provide a static helper to directly filter values (`QtPromise::each(values, functor)`). --- docs/SUMMARY.md | 2 + docs/qtpromise/api-reference.md | 2 + docs/qtpromise/helpers/each.md | 39 ++++ docs/qtpromise/qpromise/each.md | 48 +++++ src/qtpromise/qpromise.h | 3 + src/qtpromise/qpromise.inl | 23 +++ src/qtpromise/qpromisehelpers.h | 6 + tests/auto/qtpromise/helpers/each/each.pro | 4 + .../auto/qtpromise/helpers/each/tst_each.cpp | 154 ++++++++++++++++ tests/auto/qtpromise/helpers/helpers.pro | 1 + tests/auto/qtpromise/qpromise/each/each.pro | 4 + .../auto/qtpromise/qpromise/each/tst_each.cpp | 170 ++++++++++++++++++ tests/auto/qtpromise/qpromise/qpromise.pro | 1 + 13 files changed, 457 insertions(+) create mode 100644 docs/qtpromise/helpers/each.md create mode 100644 docs/qtpromise/qpromise/each.md create mode 100644 tests/auto/qtpromise/helpers/each/each.pro create mode 100644 tests/auto/qtpromise/helpers/each/tst_each.cpp create mode 100644 tests/auto/qtpromise/qpromise/each/each.pro create mode 100644 tests/auto/qtpromise/qpromise/each/tst_each.cpp diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 8f43121..a2b88ba 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -5,6 +5,7 @@ * [API Reference](qtpromise/api-reference.md) * [QPromise](qtpromise/qpromise/constructor.md) * [.delay](qtpromise/qpromise/delay.md) + * [.each](qtpromise/qpromise/each.md) * [.fail](qtpromise/qpromise/fail.md) * [.filter](qtpromise/qpromise/filter.md) * [.finally](qtpromise/qpromise/finally.md) @@ -23,5 +24,6 @@ * [qPromise](qtpromise/helpers/qpromise.md) * [qPromiseAll](qtpromise/helpers/qpromiseall.md) * [QtPromise::attempt](qtpromise/helpers/attempt.md) + * [QtPromise::each](qtpromise/helpers/each.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 ee84e52..b14b6c9 100644 --- a/docs/qtpromise/api-reference.md +++ b/docs/qtpromise/api-reference.md @@ -4,6 +4,7 @@ * [`QPromise::QPromise`](qpromise/constructor.md) * [`QPromise::delay`](qpromise/delay.md) +* [`QPromise::each`](qpromise/each.md) * [`QPromise::fail`](qpromise/fail.md) * [`QPromise::filter`](qpromise/filter.md) * [`QPromise::finally`](qpromise/finally.md) @@ -28,5 +29,6 @@ * [`qPromise`](helpers/qpromise.md) * [`qPromiseAll`](helpers/qpromiseall.md) * [`QtPromise::attempt`](helpers/attempt.md) +* [`QtPromise::each`](helpers/each.md) * [`QtPromise::filter`](helpers/filter.md) * [`QtPromise::map`](helpers/map.md) diff --git a/docs/qtpromise/helpers/each.md b/docs/qtpromise/helpers/each.md new file mode 100644 index 0000000..fd77d51 --- /dev/null +++ b/docs/qtpromise/helpers/each.md @@ -0,0 +1,39 @@ +## `QtPromise::each` + +```cpp +QtPromise::each(Sequence values, Functor functor) -> QPromise> + +// With: +// - Sequence: STL compatible container (e.g. QVector, etc.) +// - Functor: Function(T value, int index) -> void | QPromise +``` + +Calls the given `functor` on each element in `values` then resolves to the original sequence +unmodified. If `functor` throws, `output` is rejected with the new exception. + +If `functor` returns a promise (or `QFuture`), the `output` promise is delayed until all the +promises are resolved. If any of the promises fail, `output` immediately rejects with the error +of the promise that rejected, whether or not the other promises are resolved. + + +```cpp +auto output = QtPromise::each(QVector{ + QUrl("http://a..."), + QUrl("http://b..."), + QUrl("http://c...") +}, [](const QUrl& url, ...) { + return QPromise([&](auto resolve, auto reject) { + // process url asynchronously ... + }) +}); + +// `output` resolves as soon as all promises returned by +// `functor` are fulfilled or at least one is rejected. + +// output type: QPromise> +output.then([](const QVector& res) { + // 'res' contains the original values +}); +``` + +See also: [`QPromise::each`](../qpromise/each.md) diff --git a/docs/qtpromise/qpromise/each.md b/docs/qtpromise/qpromise/each.md new file mode 100644 index 0000000..e7567f7 --- /dev/null +++ b/docs/qtpromise/qpromise/each.md @@ -0,0 +1,48 @@ +## `QPromise>::each` + +> **Important:** applies only to promise with sequence value. + +```cpp +QPromise>::each(Functor functor) -> QPromise> + +// With: +// - Sequence: STL compatible container +// - Functor: Function(T value, int index) -> any +``` + +Calls the given `functor` on each element in the promise value (i.e. `Sequence`), then resolves to the original sequence unmodified. If `functor` throws, `output` is rejected with the new exception. + +```cpp +QPromise> input = {...} + +auto output = input.each([](const QByteArray& value, int index) { + // process value ... +}); + +// output type: QPromise> +output.then([](const QList& res) { + // 'res' contains the original values +}); +``` + +If `functor` returns a promise (or `QFuture`), the `output` promise is delayed until all the promises are resolved. If any of the promises fail, `output` immediately rejects with the error of the promise that rejected, whether or not the other promises are resolved. + +```cpp +QPromise> input = {...} + +auto output = input.each([](const QUrl& url, ...) { + return QPromise([&](auto resolve, auto reject) { + // process url asynchronously ... + }) +}); + +// `output` resolves as soon as all promises returned by +// `functor` are fulfilled or at least one is rejected. + +// output type: QPromise> +output.then([](const QList& res) { + // 'res' contains the original values +}); +``` + +See also: [`QtPromise::each`](../helpers/each.md) diff --git a/src/qtpromise/qpromise.h b/src/qtpromise/qpromise.h index 1c4740e..3bfbaab 100644 --- a/src/qtpromise/qpromise.h +++ b/src/qtpromise/qpromise.h @@ -88,6 +88,9 @@ public: template QPromise(F&& resolver): QPromiseBase(std::forward(resolver)) { } + template + inline QPromise each(Functor fn); + template inline QPromise filter(Functor fn); diff --git a/src/qtpromise/qpromise.inl b/src/qtpromise/qpromise.inl index b1afa7d..a645470 100644 --- a/src/qtpromise/qpromise.inl +++ b/src/qtpromise/qpromise.inl @@ -155,6 +155,29 @@ inline QPromise QPromiseBase::reject(E&& error) }); } +template +template +inline QPromise QPromise::each(Functor fn) +{ + return this->tap([=](const T& values) { + int i = 0; + + std::vector> promises; + for (const auto& v : values) { + promises.push_back( + QtPromise::attempt(fn, v, i) + .then([]() { + // Cast to void in case fn returns a non promise value. + // TODO remove when implicit cast is implemented. + })); + + i++; + } + + return QPromise::all(promises); + }); +} + template template inline QPromise QPromise::filter(Functor fn) diff --git a/src/qtpromise/qpromisehelpers.h b/src/qtpromise/qpromisehelpers.h index 0b08903..fd45d0f 100644 --- a/src/qtpromise/qpromisehelpers.h +++ b/src/qtpromise/qpromisehelpers.h @@ -64,6 +64,12 @@ attempt(Functor&& fn, Args&&... args) }); } +template +static inline QPromise each(const Sequence& values, Functor&& fn) +{ + return QPromise::resolve(values).each(std::forward(fn)); +} + template static inline typename QtPromisePrivate::PromiseMapper::PromiseType map(const Sequence& values, Functor fn) diff --git a/tests/auto/qtpromise/helpers/each/each.pro b/tests/auto/qtpromise/helpers/each/each.pro new file mode 100644 index 0000000..f823d0e --- /dev/null +++ b/tests/auto/qtpromise/helpers/each/each.pro @@ -0,0 +1,4 @@ +TARGET = tst_qpromise_each +SOURCES += $$PWD/tst_each.cpp + +include(../../qtpromise.pri) diff --git a/tests/auto/qtpromise/helpers/each/tst_each.cpp b/tests/auto/qtpromise/helpers/each/tst_each.cpp new file mode 100644 index 0000000..da4140d --- /dev/null +++ b/tests/auto/qtpromise/helpers/each/tst_each.cpp @@ -0,0 +1,154 @@ +// Tests +#include "../../shared/utils.h" + +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +class tst_helpers_each : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void emptySequence(); + void preserveValues(); + void ignoreResult(); + void delayedFulfilled(); + void delayedRejected(); + void functorThrows(); + void functorArguments(); + void sequenceTypes(); +}; + +QTEST_MAIN(tst_helpers_each) +#include "tst_each.moc" + +namespace { + +template +struct SequenceTester +{ + static void exec() + { + QVector values; + auto p = QtPromise::each(Sequence{42, 43, 44}, [&](int v, int i) { + values << i << v; + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(waitForValue(p, Sequence()), Sequence({42, 43, 44})); + QCOMPARE(values, QVector({0, 42, 1, 43, 2, 44})); + } +}; + +} // anonymous namespace + +void tst_helpers_each::emptySequence() +{ + QVector values; + auto p = QtPromise::each(QVector{}, [&](int v, ...) { + values << v; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector()); + QCOMPARE(values, QVector({})); +} + +void tst_helpers_each::preserveValues() +{ + QVector values; + auto p = QtPromise::each(QVector{42, 43, 44}, [&](int v, ...) { + values << v + 1; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QCOMPARE(values, QVector({43, 44, 45})); +} + +void tst_helpers_each::ignoreResult() +{ + QVector values; + auto p = QtPromise::each(QVector{42, 43, 44}, [&](int v, ...) { + values << v + 1; + return "Foo"; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QCOMPARE(values, QVector({43, 44, 45})); +} + +void tst_helpers_each::delayedFulfilled() +{ + QMap values; + auto p = QtPromise::each(QVector{42, 43, 44}, [&](int v, int index) { + return QPromise([&](const QPromiseResolve& resolve) { + QtPromisePrivate::qtpromise_defer([=, &values]() { + values[v] = index; + resolve(42); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QMap expected{{42, 0}, {43, 1}, {44, 2}}; + QCOMPARE(values, expected); +} + +void tst_helpers_each::delayedRejected() +{ + auto p = QtPromise::each(QVector{42, 43, 44}, [](int v, ...) { + return QPromise([&]( + const QPromiseResolve& resolve, + const QPromiseReject& reject) { + QtPromisePrivate::qtpromise_defer([=]() { + if (v == 43) { + reject(QString("foo")); + } + resolve(v); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_helpers_each::functorThrows() +{ + auto p = QtPromise::each(QVector{42, 43, 44}, [](int v, ...) { + if (v == 44) { + throw QString("foo"); + } + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_helpers_each::functorArguments() +{ + QVector values; + auto p = QtPromise::each(QVector{42, 43, 44}, [&](int v, int i) { + values << i << v; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QCOMPARE(values, QVector({0, 42, 1, 43, 2, 44})); +} + +void tst_helpers_each::sequenceTypes() +{ + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); +} diff --git a/tests/auto/qtpromise/helpers/helpers.pro b/tests/auto/qtpromise/helpers/helpers.pro index e40e04b..6c1b222 100644 --- a/tests/auto/qtpromise/helpers/helpers.pro +++ b/tests/auto/qtpromise/helpers/helpers.pro @@ -2,6 +2,7 @@ TEMPLATE = subdirs SUBDIRS += \ all \ attempt \ + each \ filter \ map \ reject \ diff --git a/tests/auto/qtpromise/qpromise/each/each.pro b/tests/auto/qtpromise/qpromise/each/each.pro new file mode 100644 index 0000000..f823d0e --- /dev/null +++ b/tests/auto/qtpromise/qpromise/each/each.pro @@ -0,0 +1,4 @@ +TARGET = tst_qpromise_each +SOURCES += $$PWD/tst_each.cpp + +include(../../qtpromise.pri) diff --git a/tests/auto/qtpromise/qpromise/each/tst_each.cpp b/tests/auto/qtpromise/qpromise/each/tst_each.cpp new file mode 100644 index 0000000..80883f1 --- /dev/null +++ b/tests/auto/qtpromise/qpromise/each/tst_each.cpp @@ -0,0 +1,170 @@ +// Tests +#include "../../shared/utils.h" + +// QtPromise +#include + +// Qt +#include + +using namespace QtPromise; + +class tst_qpromise_each : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void emptySequence(); + void preserveValues(); + void ignoreResult(); + void delayedFulfilled(); + void delayedRejected(); + void functorThrows(); + void functorArguments(); + void sequenceTypes(); +}; + +QTEST_MAIN(tst_qpromise_each) +#include "tst_each.moc" + +namespace { + +template +struct SequenceTester +{ + static void exec() + { + QVector values; + auto p = QtPromise::qPromise(Sequence{42, 43, 44}).each([&](int v, int i) { + values << i << v; + }).each([&](int v, ...) { + values << v; + return QString("foo"); + }).each([&](int v, ...) { + values << v + 1; + return QPromise::resolve(QString("foo")).then([&](){ + values << -1; + }); + }).each([&](int v, ...) { + values << v + 2; + }); + + Q_STATIC_ASSERT((std::is_same>::value)); + QCOMPARE(waitForValue(p, Sequence()), Sequence({42, 43, 44})); + + QVector expected{ + 0, 42, 1, 43, 2, 44, + 42, 43, 44, + 43, 44, 45, + -1, -1, -1, + 44, 45, 46 + }; + QCOMPARE(values, expected); + } +}; + +} // anonymous namespace + +void tst_qpromise_each::emptySequence() +{ + QVector values; + auto p = QPromise>::resolve({}).each([&](int v, ...) { + values << v; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector()); + QCOMPARE(values, QVector({})); +} + +void tst_qpromise_each::preserveValues() +{ + QVector values; + auto p = QPromise>::resolve({42, 43, 44}).each([&](int v, ...) { + values << v + 1; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QCOMPARE(values, QVector({43, 44, 45})); +} + +void tst_qpromise_each::ignoreResult() +{ + QVector values; + auto p = QPromise>::resolve({42, 43, 44}).each([&](int v, ...) { + values << v + 1; + return "Foo"; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QCOMPARE(values, QVector({43, 44, 45})); +} + +void tst_qpromise_each::delayedFulfilled() +{ + QMap values; + auto p = QPromise>::resolve({42, 43, 44}).each([&](int v, int index) { + return QPromise::resolve().delay(250).then([=, &values]() { + values[v] = index; + return 42; + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QMap expected{{42, 0}, {43, 1}, {44, 2}}; + QCOMPARE(values, expected); +} + +void tst_qpromise_each::delayedRejected() +{ + auto p = QPromise>::resolve({42, 43, 44}).each([](int v, ...) { + return QPromise([&]( + const QPromiseResolve& resolve, + const QPromiseReject& reject) { + QtPromisePrivate::qtpromise_defer([=]() { + if (v == 44) { + reject(QString("foo")); + } + resolve(v); + }); + }); + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_qpromise_each::functorThrows() +{ + auto p = QPromise>::resolve({42, 43, 44}).each([](int v, ...) { + if (v == 44) { + throw QString("foo"); + } + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForError(p, QString()), QString("foo")); +} + +void tst_qpromise_each::functorArguments() +{ + QVector values; + auto p = QPromise>::resolve({42, 43, 44}).each([&](int v, int i) { + values << i << v; + }); + + Q_STATIC_ASSERT((std::is_same>>::value)); + QCOMPARE(waitForValue(p, QVector()), QVector({42, 43, 44})); + QCOMPARE(values, QVector({0, 42, 1, 43, 2, 44})); +} + +void tst_qpromise_each::sequenceTypes() +{ + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); + SequenceTester>::exec(); +} diff --git a/tests/auto/qtpromise/qpromise/qpromise.pro b/tests/auto/qtpromise/qpromise/qpromise.pro index e555dd0..e4b617d 100644 --- a/tests/auto/qtpromise/qpromise/qpromise.pro +++ b/tests/auto/qtpromise/qpromise/qpromise.pro @@ -2,6 +2,7 @@ TEMPLATE = subdirs SUBDIRS += \ construct \ delay \ + each \ fail \ filter \ finally \