From 18739bd8e0404bf8f02bc5b84e04536d4743808e Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 11 Feb 2018 16:19:03 +0100 Subject: [PATCH] New documentation based on GitBook CLI Split the root README.md in multiple Markdown files (in the `docs/` folder) to make easier reading, editing and extending the documentation. An online version is also available on netlify (https://qtpromise.netlify.com). Building it requires Node.js installed, then: - npm install -g gitbook-cli - gitbook install ./ - gitbook build . dist/docs --- .gitignore | 3 + .travis.yml | 4 + README.md | 429 +------------------------ book.json | 40 +++ docs/README.md | 15 + docs/SUMMARY.md | 20 ++ docs/assets/style.css | 15 + docs/qtpromise/api-reference.md | 25 ++ docs/qtpromise/getting-started.md | 93 ++++++ docs/qtpromise/helpers/qpromise.md | 15 + docs/qtpromise/helpers/qpromiseall.md | 16 + docs/qtpromise/qpromise/all.md | 26 ++ docs/qtpromise/qpromise/constructor.md | 29 ++ docs/qtpromise/qpromise/delay.md | 14 + docs/qtpromise/qpromise/fail.md | 19 ++ docs/qtpromise/qpromise/finally.md | 15 + docs/qtpromise/qpromise/isfulfilled.md | 7 + docs/qtpromise/qpromise/ispending.md | 7 + docs/qtpromise/qpromise/isrejected.md | 8 + docs/qtpromise/qpromise/reject.md | 20 ++ docs/qtpromise/qpromise/resolve.md | 22 ++ docs/qtpromise/qpromise/tap.md | 18 ++ docs/qtpromise/qpromise/then.md | 87 +++++ docs/qtpromise/qpromise/wait.md | 21 ++ docs/qtpromise/qtconcurrent.md | 66 ++++ docs/qtpromise/thread-safety.md | 5 + 26 files changed, 617 insertions(+), 422 deletions(-) create mode 100644 book.json create mode 100644 docs/README.md create mode 100644 docs/SUMMARY.md create mode 100644 docs/assets/style.css create mode 100644 docs/qtpromise/api-reference.md create mode 100644 docs/qtpromise/getting-started.md create mode 100644 docs/qtpromise/helpers/qpromise.md create mode 100644 docs/qtpromise/helpers/qpromiseall.md create mode 100644 docs/qtpromise/qpromise/all.md create mode 100644 docs/qtpromise/qpromise/constructor.md create mode 100644 docs/qtpromise/qpromise/delay.md create mode 100644 docs/qtpromise/qpromise/fail.md create mode 100644 docs/qtpromise/qpromise/finally.md create mode 100644 docs/qtpromise/qpromise/isfulfilled.md create mode 100644 docs/qtpromise/qpromise/ispending.md create mode 100644 docs/qtpromise/qpromise/isrejected.md create mode 100644 docs/qtpromise/qpromise/reject.md create mode 100644 docs/qtpromise/qpromise/resolve.md create mode 100644 docs/qtpromise/qpromise/tap.md create mode 100644 docs/qtpromise/qpromise/then.md create mode 100644 docs/qtpromise/qpromise/wait.md create mode 100644 docs/qtpromise/qtconcurrent.md create mode 100644 docs/qtpromise/thread-safety.md diff --git a/.gitignore b/.gitignore index dd3d84a..c360d95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +dist +node_modules *.gcno *.gcda *.moc @@ -9,4 +11,5 @@ Makefile* moc_*.cpp moc_*.h coverage.info +package-lock.json target_wrapper.bat diff --git a/.travis.yml b/.travis.yml index eff7ef1..f9d4c7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,7 @@ script: after_success: - bash <(curl -s https://codecov.io/bash) -f coverage.info + + +# gitbook install . +# gitbook build . dist/docs diff --git a/README.md b/README.md index ce7efea..476ff50 100644 --- a/README.md +++ b/README.md @@ -1,433 +1,18 @@ -Promises/A+ +Promises/A+ # QtPromise -[![qpm](https://img.shields.io/github/release/simonbrunel/qtpromise.svg?style=flat-square&label=qpm&colorB=4CAF50)](http://www.qpm.io/packages/com.github.simonbrunel.qtpromise/index.html) [![Travis](https://img.shields.io/travis/simonbrunel/qtpromise/master.svg?style=flat-square)](https://travis-ci.org/simonbrunel/qtpromise) [![coverage](https://img.shields.io/codecov/c/github/simonbrunel/qtpromise.svg?style=flat-square)](https://codecov.io/gh/simonbrunel/qtpromise) +[![qpm](https://img.shields.io/github/release/simonbrunel/qtpromise.svg?style=flat-square&label=qpm&colorB=4CAF50)](https://www.qpm.io/packages/com.github.simonbrunel.qtpromise/index.html) [![Travis](https://img.shields.io/travis/simonbrunel/qtpromise/master.svg?style=flat-square)](https://travis-ci.org/simonbrunel/qtpromise) [![coverage](https://img.shields.io/codecov/c/github/simonbrunel/qtpromise.svg?style=flat-square)](https://codecov.io/gh/simonbrunel/qtpromise) [Promises/A+](https://promisesaplus.com/) implementation for [Qt/C++](https://www.qt.io/). 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`. +## Documentation -### qpm -Alternatively and **only** if your project relies on [qpm](http://www.qpm.io/), you can install QtPromise as follow: - -```bash -qpm install com.github.simonbrunel.qtpromise -``` - -### 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 handle 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 = qPromise(future); -``` - -or simply: - -```cpp -auto promise = qPromise(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) { - // {...} -}); -``` - -## Thread-Safety - QPromise is thread-safe and can be copied and accessed across different threads. QPromise relies on [explicitly data sharing](http://doc.qt.io/qt-5/qexplicitlyshareddatapointer.html#details) and thus `auto p2 = p1` represents the same promise: when `p1` resolves, handlers registered on `p1` and `p2` are called, the fulfilled value being shared between both instances. - -> **Note:** while it's safe to access the resolved value from different threads using [`then`](#qpromise-then), QPromise provides no guarantee about the object being pointed to. Thread-safety and reentrancy rules for that object still apply. - -## 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::tap(handler) -> QPromise` -This `handler` allows to observe the value of the `input` promise, without changing the propagated value. The `output` promise will be resolved with the same value as the `input` promise (the `handler` returned value will be ignored). However, if `handler` throws, `output` is rejected with the new exception. Unlike [`finally`](#qpromise-finally), this handler is **not** called for rejections. - -```cpp -QPromise input = {...} -auto output = input.tap([](int res) { - log(res); -}).then([](int res) { - // {...} -}); -``` - -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::delay(handler) -> QPromise` -This method returns a promise that will be fulfilled with the same value as the `input` promise and after at least `msec` milliseconds. If the `input` promise is rejected, the `output` promise is immediately rejected with the same reason. - -```cpp -QPromise input = {...} -auto output = input.delay(2000).then([](int res) { - // called 2 seconds after `input` is fulfilled -}); -``` - -### `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(Sequence>) -> 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. - -`Sequence` is any STL compatible container (eg. `QVector`, `QList`, `std::vector`, etc.) - -```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(Sequence 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) -``` +* [Getting Started](https://qtpromise.netlify.com/qtpromise/getting-started) +* [Thread-Safety](https://qtpromise.netlify.com/qtpromise/thread-safety) +* [QtConcurrent](https://qtpromise.netlify.com/qtpromise/qtconcurrent) +* [API Reference](https://qtpromise.netlify.com/qtpromise/api-reference) ## License QtPromise is available under the [MIT license](LICENSE). diff --git a/book.json b/book.json new file mode 100644 index 0000000..6687058 --- /dev/null +++ b/book.json @@ -0,0 +1,40 @@ +{ + "title": "QtPromise", + "description": "Promises/A+ implementation for Qt/C++", + "author": "Simon Brunel", + "gitbook": "3.2.3", + "root": "docs", + "plugins": [ + "-lunr", + "-search", + "search-plus", + "anchorjs", + "edit-link", + "expand-active-chapter", + "ga", + "github" + ], + "pluginsConfig": { + "anchorjs": { + "icon": "#", + "placement": "left", + "visible": "always" + }, + "edit-link": { + "base": "https://github.com/simonbrunel/qtpromise/edit/master/docs" + }, + "ga": { + "token": "UA-113899811-1", + "configuration": "auto" + }, + "github": { + "url": "https://github.com/simonbrunel/qtpromise" + }, + "theme-default": { + "showLevel": false, + "styles": { + "website": "assets/style.css" + } + } + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4ca6ccb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +Promises/A+ + +# QtPromise +[Promises/A+](https://promisesaplus.com/) implementation for [Qt/C++](https://www.qt.io/). + +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). + +## QtPromise for C++ +* [Getting Started](qtpromise/getting-started.md) +* [Thread-Safety](qtpromise/thread-safety.md) +* [QtConcurrent](qtpromise/qtconcurrent.md) +* [API Reference](qtpromise/api-reference.md) + +## License +QtPromise is available under the [MIT license](https://github.com/simonbrunel/qtpromise/blob/master/LICENSE). diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..f31184b --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,20 @@ +### QtPromise for C++ +* [Getting Started](qtpromise/getting-started.md) +* [QtConcurrent](qtpromise/qtconcurrent.md) +* [Thread-Safety](qtpromise/thread-safety.md) +* [API Reference](qtpromise/api-reference.md) + * [QPromise](qtpromise/qpromise/constructor.md) + * [.delay](qtpromise/qpromise/delay.md) + * [.fail](qtpromise/qpromise/fail.md) + * [.finally](qtpromise/qpromise/finally.md) + * [.isFulfilled](qtpromise/qpromise/isfulfilled.md) + * [.isPending](qtpromise/qpromise/ispending.md) + * [.isRejected](qtpromise/qpromise/isrejected.md) + * [.tap](qtpromise/qpromise/tap.md) + * [.then](qtpromise/qpromise/then.md) + * [.wait](qtpromise/qpromise/wait.md) + * [::all (static)](qtpromise/qpromise/all.md) + * [::reject (static)](qtpromise/qpromise/reject.md) + * [::resolve (static)](qtpromise/qpromise/resolve.md) + * [qPromise](qtpromise/helpers/qpromise.md) + * [qPromiseAll](qtpromise/helpers/qpromiseall.md) diff --git a/docs/assets/style.css b/docs/assets/style.css new file mode 100644 index 0000000..acdd3e5 --- /dev/null +++ b/docs/assets/style.css @@ -0,0 +1,15 @@ +a.anchorjs-link { + color: rgba(65, 131, 196, 0.1); + font-weight: 400; + text-decoration: none; + transition: color 100ms ease-out; + z-index: 999; +} + +a.anchorjs-link:hover { + color: rgba(65, 131, 196, 1); +} + +sup { + font-size: 0.75em !important; +} diff --git a/docs/qtpromise/api-reference.md b/docs/qtpromise/api-reference.md new file mode 100644 index 0000000..6bb5c44 --- /dev/null +++ b/docs/qtpromise/api-reference.md @@ -0,0 +1,25 @@ +## QPromise + +### Public Members + +* [`QPromise::QPromise`](qpromise/constructor.md) +* [`QPromise::delay`](qpromise/delay.md) +* [`QPromise::fail`](qpromise/fail.md) +* [`QPromise::finally`](qpromise/finally.md) +* [`QPromise::isFulfilled`](qpromise/isfulfilled.md) +* [`QPromise::isPending`](qpromise/ispending.md) +* [`QPromise::isRejected`](qpromise/isrejected.md) +* [`QPromise::tap`](qpromise/tap.md) +* [`QPromise::then`](qpromise/then.md) +* [`QPromise::wait`](qpromise/wait.md) + +### Public Static Members + +* [`[static] QPromise::all`](qpromise/all.md) +* [`[static] QPromise::reject`](qpromise/reject.md) +* [`[static] QPromise::resolve`](qpromise/resolve.md) + +## Helpers + +* [`qPromise`](helpers/qpromise.md) +* [`qPromiseAll`](helpers/qpromiseall.md) diff --git a/docs/qtpromise/getting-started.md b/docs/qtpromise/getting-started.md new file mode 100644 index 0000000..36548bc --- /dev/null +++ b/docs/qtpromise/getting-started.md @@ -0,0 +1,93 @@ +## 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`. + +## qpm + +Alternatively and **only** if your project relies on [qpm](https://www.qpm.io/), you can install QtPromise as follow: + +```bash +qpm install com.github.simonbrunel.qtpromise +``` + +## 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/constructor.md) 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.md): + +```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.md) *and only if download succeeded*, uncompress received data, +- [`then`](qpromise/then.md) validate and process the uncompressed entries, +- [`finally`](qpromise/finally.md) perform operations whatever the process succeeded or failed, +- and handle specific errors using [`fail`](qpromise/fail.md). + +```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 +}); +``` diff --git a/docs/qtpromise/helpers/qpromise.md b/docs/qtpromise/helpers/qpromise.md new file mode 100644 index 0000000..56c3996 --- /dev/null +++ b/docs/qtpromise/helpers/qpromise.md @@ -0,0 +1,15 @@ +## `qPromise` + +``` +qPromise(T value) -> QPromise +``` + +Similar to the [`QPromise::resolve`](../qpromise/resolve.md) 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.md#convert)). diff --git a/docs/qtpromise/helpers/qpromiseall.md b/docs/qtpromise/helpers/qpromiseall.md new file mode 100644 index 0000000..9d9741f --- /dev/null +++ b/docs/qtpromise/helpers/qpromiseall.md @@ -0,0 +1,16 @@ +## `qPromiseAll` + +``` +qPromiseAll(Sequence> promises) -> QPromise> +qPromiseAll(Sequence> promises) -> QPromise +``` + +This method simply calls the appropriated [`QPromise::all`](../qpromise/all.md) 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) +``` + diff --git a/docs/qtpromise/qpromise/all.md b/docs/qtpromise/qpromise/all.md new file mode 100644 index 0000000..88b89c2 --- /dev/null +++ b/docs/qtpromise/qpromise/all.md @@ -0,0 +1,26 @@ +## `[static] QPromise::all` + +``` +[static] QPromise::all(Sequence> promises) -> 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. + +`Sequence` is any STL compatible container (eg. `QVector`, `QList`, `std::vector`, etc.) + +```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.md) diff --git a/docs/qtpromise/qpromise/constructor.md b/docs/qtpromise/qpromise/constructor.md new file mode 100644 index 0000000..302ed20 --- /dev/null +++ b/docs/qtpromise/qpromise/constructor.md @@ -0,0 +1,29 @@ +## `QPromise::QPromise` + +``` +QPromise::QPromise(Function 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) { + // {...} +}); +``` diff --git a/docs/qtpromise/qpromise/delay.md b/docs/qtpromise/qpromise/delay.md new file mode 100644 index 0000000..21af63d --- /dev/null +++ b/docs/qtpromise/qpromise/delay.md @@ -0,0 +1,14 @@ +## `QPromise::delay` + +``` +QPromise::delay(int msec) -> QPromise +``` + +This method returns a promise that will be fulfilled with the same value as the `input` promise and after at least `msec` milliseconds. If the `input` promise is rejected, the `output` promise is immediately rejected with the same reason. + +```cpp +QPromise input = {...} +auto output = input.delay(2000).then([](int res) { + // called 2 seconds after `input` is fulfilled +}); +``` diff --git a/docs/qtpromise/qpromise/fail.md b/docs/qtpromise/qpromise/fail.md new file mode 100644 index 0000000..5b6d8ec --- /dev/null +++ b/docs/qtpromise/qpromise/fail.md @@ -0,0 +1,19 @@ +## `QPromise::fail` + +``` +QPromise::fail(Function 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 +}); +``` diff --git a/docs/qtpromise/qpromise/finally.md b/docs/qtpromise/qpromise/finally.md new file mode 100644 index 0000000..671c957 --- /dev/null +++ b/docs/qtpromise/qpromise/finally.md @@ -0,0 +1,15 @@ +## `QPromise::finally` + +``` +QPromise::finally(Function 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. diff --git a/docs/qtpromise/qpromise/isfulfilled.md b/docs/qtpromise/qpromise/isfulfilled.md new file mode 100644 index 0000000..8b25e00 --- /dev/null +++ b/docs/qtpromise/qpromise/isfulfilled.md @@ -0,0 +1,7 @@ +# `QPromise::isFulfilled` + +``` +QPromise::isFulfilled() -> bool +``` + +Returns `true` if the promise is fulfilled, otherwise returns `false`. diff --git a/docs/qtpromise/qpromise/ispending.md b/docs/qtpromise/qpromise/ispending.md new file mode 100644 index 0000000..4fb5c15 --- /dev/null +++ b/docs/qtpromise/qpromise/ispending.md @@ -0,0 +1,7 @@ +# `QPromise::isPending` + +``` +QPromise::isPending() -> bool +``` + +Returns `true` if the promise is pending (not fulfilled or rejected), otherwise returns `false`. diff --git a/docs/qtpromise/qpromise/isrejected.md b/docs/qtpromise/qpromise/isrejected.md new file mode 100644 index 0000000..e982e51 --- /dev/null +++ b/docs/qtpromise/qpromise/isrejected.md @@ -0,0 +1,8 @@ +# `QPromise::isRejected` + +``` +QPromise::isRejected() -> bool +``` + +Returns `true` if the promise is rejected, otherwise returns `false`. + diff --git a/docs/qtpromise/qpromise/reject.md b/docs/qtpromise/qpromise/reject.md new file mode 100644 index 0000000..3eb0e8f --- /dev/null +++ b/docs/qtpromise/qpromise/reject.md @@ -0,0 +1,20 @@ +## `[static] QPromise::reject` + +``` +[static] QPromise::reject(any 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) { + // {...} + }); +} +``` diff --git a/docs/qtpromise/qpromise/resolve.md b/docs/qtpromise/qpromise/resolve.md new file mode 100644 index 0000000..3d49f7f --- /dev/null +++ b/docs/qtpromise/qpromise/resolve.md @@ -0,0 +1,22 @@ +## `[static] QPromise::resolve` + +``` +[static] QPromise::resolve(T 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.md) diff --git a/docs/qtpromise/qpromise/tap.md b/docs/qtpromise/qpromise/tap.md new file mode 100644 index 0000000..fe93620 --- /dev/null +++ b/docs/qtpromise/qpromise/tap.md @@ -0,0 +1,18 @@ +## `QPromise::tap` + +``` +QPromise::tap(Function handler) -> QPromise +``` + +This `handler` allows to observe the value of the `input` promise, without changing the propagated value. The `output` promise will be resolved with the same value as the `input` promise (the `handler` returned value will be ignored). However, if `handler` throws, `output` is rejected with the new exception. Unlike [`finally`](finally.md), this handler is **not** called for rejections. + +```cpp +QPromise input = {...} +auto output = input.tap([](int res) { + log(res); +}).then([](int res) { + // {...} +}); +``` + +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. diff --git a/docs/qtpromise/qpromise/then.md b/docs/qtpromise/qpromise/then.md new file mode 100644 index 0000000..be55e23 --- /dev/null +++ b/docs/qtpromise/qpromise/then.md @@ -0,0 +1,87 @@ +## `QPromise::then` + +``` +QPromise::then(Function onFulfilled, Function onRejected) -> QPromise +QPromise::then(Function onFulfilled) -> 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`](fail.md) 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. diff --git a/docs/qtpromise/qpromise/wait.md b/docs/qtpromise/qpromise/wait.md new file mode 100644 index 0000000..e8795d0 --- /dev/null +++ b/docs/qtpromise/qpromise/wait.md @@ -0,0 +1,21 @@ +## `QPromise::wait` + +``` +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 +``` diff --git a/docs/qtpromise/qtconcurrent.md b/docs/qtpromise/qtconcurrent.md new file mode 100644 index 0000000..c894790 --- /dev/null +++ b/docs/qtpromise/qtconcurrent.md @@ -0,0 +1,66 @@ +## QtConcurrent + +QtPromise integrates with [QtConcurrent](https://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.md) helper: + +```cpp +QFuture future = QtConcurrent::run([]() { + // {...} + return 42; +}); + +QPromise promise = qPromise(future); +``` + +or simply: + +```cpp +auto promise = qPromise(QtConcurrent::run([]() { + // {...} +})); +``` + +## Chain + +Returning a `QFuture` in [`then`](qpromise/then.md) or [`fail`](qpromise/fail.md) 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](https://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`](https://doc.qt.io/qt-5/qunhandledexception.html#details) (this restriction only applies to exceptions thrown from a QtConcurrent thread, [read more](https://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) { + // {...} +}); +``` diff --git a/docs/qtpromise/thread-safety.md b/docs/qtpromise/thread-safety.md new file mode 100644 index 0000000..b4eee3c --- /dev/null +++ b/docs/qtpromise/thread-safety.md @@ -0,0 +1,5 @@ +## Thread-Safety + +QPromise is thread-safe and can be copied and accessed across different threads. QPromise relies on [explicitly data sharing](https://doc.qt.io/qt-5/qexplicitlyshareddatapointer.html#details) and thus `auto p2 = p1` represents the same promise: when `p1` resolves, handlers registered on `p1` and `p2` are called, the fulfilled value being shared between both instances. + +> **Note:** while it's safe to access the resolved value from different threads using [`then`](qpromise/then.md), QPromise provides no guarantee about the object being pointed to. Thread-safety and reentrancy rules for that object still apply.