diff --git a/.travis.yml b/.travis.yml index 51742dc..eff7ef1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_script: script: - qmake qtpromise.pro CONFIG+=coverage - make -j4 - - make -j4 check --quiet + - make check --quiet - lcov -capture --directory . --o coverage.info - lcov -e coverage.info '**/src/**/*' -o coverage.info diff --git a/LICENSE.md b/LICENSE.md index e1f86a6..f5e6e9c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -**The MIT License (MIT)** +The MIT License (MIT) Copyright (c) 2017 Simon Brunel diff --git a/README.md b/README.md index 8b6d92a..b66e271 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,391 @@ [Promises/A+](https://promisesaplus.com/) implementation for [Qt/C++](https://www.qt.io/). -Requires [Qt 5.4](https://www.qt.io/download/) or later. +Requires [Qt 5.4](https://www.qt.io/download/) (or later) with [C++11 support enabled](https://wiki.qt.io/How_to_use_C++11_in_your_Qt_Projects). + +## Getting Started +### Installation +QtPromise is a [header-only](https://en.wikipedia.org/wiki/Header-only) library, simply download the [latest release](https://github.com/simonbrunel/qtpromise/releases/latest) (or [`git submodule`](https://git-scm.com/docs/git-submodule])) and include `qtpromise.pri` from your project `.pro`. + +### Usage +The recommended way to use QtPromise is to include the single module header: + +```cpp +#include +``` + +### Example +Let's first make the code more readable by using the library namespace: + +```cpp +using namespace QtPromise; +``` + +This `download` function creates a [promise from callbacks](#qpromise-qpromise) which will be resolved when the network request is finished: + +```cpp +QPromise download(const QUrl& url) +{ + return QPromise([&]( + const QPromiseResolve& resolve, + const QPromiseReject& reject) { + + QNetworkReply* reply = manager->get(QNetworkRequest(url)); + QObject::connect(reply, &QNetworkReply::finished, [=]() { + if (reply->error() == QNetworkReply::NoError) { + resolve(reply->readAll()); + } else { + reject(reply->error()); + } + + reply->deleteLater(); + }); + }); +} +``` + +The following method `uncompress` data in a separate thread and returns a [promise from QFuture](#qtconcurrent): + +```cpp +QPromise uncompress(const QByteArray& data) +{ + return qPromise(QtConcurrent::run([](const QByteArray& data) { + Entries entries; + + // {...} uncompress data and parse content. + + if (error) { + throw MalformedException(); + } + + return entries; + }, data)); +} +``` + +It's then easy to chain the whole asynchronous process using promises: +- initiate the promise chain by downloading a specific URL, +- [`then`](#qpromise-then) *and only if download succeeded*, uncompress received data, +- [`then`](#qpromise-then) validate and process the uncompressed entries, +- [`finally`](#qpromise-finally) perform operations whatever the process succeeded or failed, +- and hande specific errors using [`fail`](#qpromise-fail). + +```cpp +download(url).then(&uncompress).then([](const Entries& entries) { + if (entries.isEmpty()) { + throw UpdateException("No entries"); + } + // {...} process entries +}).finally([]() { + // {...} cleanup +}).fail([](QNetworkReply::NetworkError err) { + // {...} handle network error +}).fail([](const UpdateException& err) { + // {...} handle update error +}).fail([]() { + // {...} catch all +}); +``` + +## QtConcurrent +QtPromise integrates with [QtConcurrent](http://doc.qt.io/qt-5/qtconcurrent-index.html) to make easy chaining QFuture with QPromise. + +### Convert +Converting `QFuture` to `QPromise` is done using the [`qPromise`](#helpers-qpromise) helper: + +```cpp +QFuture future = QtConcurrent::run([]() { + // {...} + return 42; +}); + +QPromise promise = qtPromise(future); +``` + +or simply: + +```cpp +auto promise = qtPromise(QtConcurrent::run([]() { + // {...} +})); +``` + +### Chain +Returning a `QFuture` in [`then`](#qpromise-then) or [`fail`](#qpromise-fail) automatically translate to `QPromise`: + +```cpp +QPromise input = ... +auto output = input.then([](int res) { + return QtConcurrent::run([]() { + // {...} + return QString("42"); + }); +}); + +// output type: QPromise +output.then([](const QString& res) { + // {...} +}); +``` + +The `output` promise is resolved when the `QFuture` is [finished](http://doc.qt.io/qt-5/qfuture.html#isFinished). + +### Error +Exceptions thrown from a QtConcurrent thread reject the associated promise with the exception as the reason. Note that if you throw an exception that is not a subclass of `QException`, the promise with be rejected with [`QUnhandledException`](http://doc.qt.io/qt-5/qunhandledexception.html#details) (this restriction only applies to exceptions thrown from a QtConcurrent thread, [read more](http://doc.qt.io/qt-5/qexception.html#details)). + +```cpp +QPromise promise = ... +promise.then([](int res) { + return QtConcurrent::run([]() { + // {...} + + if (!success) { + throw CustomException(); + } + + return QString("42"); + }); +}).fail(const CustomException& err) { + // {...} +}); +``` + +## QPromise +### `QPromise::QPromise(resolver)` +Creates a new promise that will be fulfilled or rejected by the given `resolver` lambda: + +```cpp +QPromise promise([](const QPromiseResolve& resolve, const QPromiseReject& reject) { + async_method([=](bool success, int result) { + if (success) { + resolve(result); + } else { + reject(customException()); + } + }); +}); +``` + +> **Note:** `QPromise` is specialized to not contain any value, meaning that the `resolve` callback takes no argument. + +**C++14** + +```cpp +QPromise promise([](const auto& resolve, const auto& reject) { + // {...} +}); +``` + +### `QPromise::then(onFulfilled, onRejected) -> QPromise` +See [Promises/A+ `.then`](https://promisesaplus.com/#the-then-method) for details. + +```cpp +QPromise input = ... +auto output = input.then([](int res) { + // called with the 'input' result if fulfilled +}, [](const ReasonType& reason) { + // called with the 'input' reason if rejected + // see QPromise::fail for details +}); +``` + +> **Note**: `onRejected` handler is optional, `output` will be rejected with the same reason as `input`. + +> **Note**: it's recommended to use the [`fail`](#qpromise-fail) shorthand to handle errors. + +The type `` of the `output` promise depends on the return type of the `onFulfilled` handler: + +```cpp +QPromise input = {...} +auto output = input.then([](int res) { + return QString::number(res); // -> QPromise +}); + +// output type: QPromise +output.then([](const QString& res) { + // {...} +}); +``` + +> **Note**: only `onFulfilled` can change the promise type, `onRejected` **must** return the same type as `onFulfilled`. That also means if `onFulfilled` is `nullptr`, `onRejected` must return the same type as the `input` promise. + +```cpp +QPromise input = ... +auto output = input.then([](int res) { + return res + 4; +}, [](const ReasonType& reason) { + return -1; +}); +``` + +If `onFulfilled` doesn't return any value, the `output` type is `QPromise`: + +```cpp +QPromise input = ... +auto output = input.then([](int res) { + // {...} +}); + +// output type: QPromise +output.then([]() { + // `QPromise` `onFulfilled` handler has no argument +}); +``` + +You can also decide to skip the promise result by omitting the handler argument: + +```cpp +QPromise input = {...} +auto output = input.then([]( /* skip int result */ ) { + // {...} +}); +``` + +The `output` promise can be *rejected* by throwing an exception in either `onFulfilled` or `onRejected`: + +```cpp +QPromise input = {...} +auto output = input.then([](int res) { + if (res == -1) { + throw ReasonType(); + } else { + return res; + } +}); + +// output.isRejected() is true +``` + +If an handler returns a promise (or QFuture), the `output` promise is delayed and will be resolved by the returned promise. + +### `QPromise::fail(onRejected) -> QPromise` +Shorthand to `promise.then(nullptr, onRejected)`, similar to the [`catch` statement](http://en.cppreference.com/w/cpp/language/try_catch): + +```cpp +promise.fail([](const MyException&) { + // {...} +}).fail(const QException&) { + // {...} +}).fail(const std::exception&) { + // {...} +}).fail() { + // {...} catch-all +}); +``` + +### `QPromise::finally(handler) -> QPromise` +This `handler` is **always** called, without any argument and whatever the `input` promise state (fulfilled or rejected). The `output` promise has the same type as the `input` one but also the same value or error. The finally `handler` **can not modify the fulfilled value** (the returned value is ignored), however, if `handler` throws, `output` is rejected with the new exception. + +```cpp +auto output = input.finally([]() { + // {...} +}); +``` + +If `handler` returns a promise (or QFuture), the `output` promise is delayed until the returned promise is resolved and under the same conditions: the delayed value is ignored, the error transmitted to the `output` promise. + +### `QPromise::wait() -> QPromise` +This method holds the execution of the remaining code **without** blocking the event loop of the current thread: + +```cpp +int result = -1; +QPromise input = qPromise(QtConcurrent::run([]() { return 42; })); +auto output = input.then([&](int res) { + result = res; +}); + +// output.isPending() is true && result is -1 + +output.wait(); + +// output.isPending() is false && result is 42 +``` + +### `QPromise::isPending() -> bool` +Returns `true` if the promise is pending (not fulfilled or rejected), otherwise returns `false`. + +### `QPromise::isFulfilled() -> bool` +Returns `true` if the promise is fulfilled, otherwise returns `false`. + +### `QPromise::isRejected() -> bool` +Returns `true` if the promise is rejected, otherwise returns `false`. + +## QPromise (statics) +### `[static] QPromise::resolve(value) -> QPromise` +Creates a `QPromise` that is fulfilled with the given `value` of type `T`: + +```cpp +QPromise compute(const QString& type) +{ + if (type == "magic") { + return QPromise::resolve(42); + } + + return QPromise([](const QPromiseResolve& resolve) { + // {...} + }); +} +``` + +See also: [`qPromise`](#helpers-qpromise) + +### `[static] QPromise::reject(reason) -> QPromise` +Creates a `QPromise` that is rejected with the given `reason` of *whatever type*: + +```cpp +QPromise compute(const QString& type) +{ + if (type == "foobar") { + return QPromise::reject(QString("Unknown type: %1").arg(type)); + } + + return QPromise([](const QPromiseResolve& resolve) { + // {...} + }); +} +``` + +### `[static] QPromise::all(QVector>) -> QPromise>` +Returns a `QPromise>` that fulfills when **all** `promises` of (the same) type `T` have been fulfilled. The `output` value is a vector containing **all** the values of `promises`, in the same order. If any of the given `promises` fail, `output` immediately rejects with the error of the promise that rejected, whether or not the other promises are resolved. + +```cpp +QVector > promises{ + download(QUrl("http://a...")), + download(QUrl("http://b...")), + download(QUrl("http://c...")) +}; + +auto output = QPromise::all(promises); + +// output type: QPromise> +output.then([](const QVector& res) { + // {...} +}); +``` + +See also: [`qPromiseAll`](#helpers-qpromiseall) + +## Helpers +### `qPromise(T value) -> QPromise` +Similar to the `QPromise::resolve` static method, creates a promise resolved from a given `value` without the extra typing: + +```cpp +auto promise = qPromise(); // QPromise +auto promise = qPromise(42); // QPromise +auto promise = qPromise(QString("foo")); // QPromise +``` + +This method also allows to convert `QFuture` to `QPromise` delayed until the `QFuture` is finished ([read more](#qtconcurrent-convert)). + +### `qPromiseAll(QVector promises) -> QPromise>` +This method simply calls the appropriated [`QPromise::all`](#qpromise-all) static method based on the given `QVector` type. In some cases, this method is more convenient than the static one since it avoid some extra typing: + +```cpp +QVector > promises{...} + +auto output = qPromiseAll(promises); +// eq. QPromise::all(promises) +``` ## License QtPromise is available under the [MIT license](LICENSE.md).