From 991c759f25011e07e221c2faed5d022951a805fc Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 30 Jun 2019 12:06:39 +0700 Subject: [PATCH 1/7] add callback param to request_builder --- headers/curly.hpp/curly.hpp | 23 +++++++++++++++++++---- sources/curly.cpp | 14 +++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/headers/curly.hpp/curly.hpp b/headers/curly.hpp/curly.hpp index 8d077e3..675b7fc 100644 --- a/headers/curly.hpp/curly.hpp +++ b/headers/curly.hpp/curly.hpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace curly_hpp { @@ -48,6 +49,9 @@ namespace curly_hpp using time_ms_t = std::chrono::milliseconds; using time_point_t = std::chrono::steady_clock::time_point; + class request; + using callback_t = std::function; + class upload_handler { public: virtual ~upload_handler() {} @@ -176,6 +180,7 @@ namespace curly_hpp request_builder& content(std::string_view b); request_builder& content(content_t b) noexcept; + request_builder& callback(callback_t c) noexcept; request_builder& uploader(uploader_uptr u) noexcept; request_builder& downloader(downloader_uptr d) noexcept; @@ -193,6 +198,9 @@ namespace curly_hpp content_t& content() noexcept; const content_t& content() const noexcept; + callback_t& callback() noexcept; + const callback_t& callback() const noexcept; + uploader_uptr& uploader() noexcept; const uploader_uptr& uploader() const noexcept; @@ -201,16 +209,22 @@ namespace curly_hpp request send(); + template < typename Callback > + request_builder& callback(Callback&& f) { + static_assert(std::is_convertible_v); + return callback(callback_t(std::forward(f))); + } + template < typename Uploader, typename... Args > request_builder& uploader(Args&&... args) { - return uploader(std::make_unique( - std::forward(args)...)); + static_assert(std::is_base_of_v); + return uploader(std::make_unique(std::forward(args)...)); } template < typename Downloader, typename... Args > request_builder& downloader(Args&&... args) { - return downloader(std::make_unique( - std::forward(args)...)); + static_assert(std::is_base_of_v); + return downloader(std::make_unique(std::forward(args)...)); } private: std::string url_; @@ -224,6 +238,7 @@ namespace curly_hpp time_sec_t connection_timeout_{20u}; private: content_t content_; + callback_t callback_; uploader_uptr uploader_; downloader_uptr downloader_; }; diff --git a/sources/curly.cpp b/sources/curly.cpp index 5aaf9c4..6debdbc 100644 --- a/sources/curly.cpp +++ b/sources/curly.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #ifndef NOMINMAX @@ -759,6 +758,11 @@ namespace curly_hpp return *this; } + request_builder& request_builder::callback(callback_t c) noexcept { + callback_ = std::move(c); + return *this; + } + request_builder& request_builder::uploader(uploader_uptr u) noexcept { uploader_ = std::move(u); return *this; @@ -813,6 +817,14 @@ namespace curly_hpp return content_; } + callback_t& request_builder::callback() noexcept { + return callback_; + } + + const callback_t& request_builder::callback() const noexcept { + return callback_; + } + uploader_uptr& request_builder::uploader() noexcept { return uploader_; } From dcea9f99511ef4f1630c329d41ee4d5cdf2ba37a Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 30 Jun 2019 12:38:09 +0700 Subject: [PATCH 2/7] add is_done/is_pending functions to request --- headers/curly.hpp/curly.hpp | 3 +++ sources/curly.cpp | 20 +++++++++++++++++++- untests/curly_tests.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/headers/curly.hpp/curly.hpp b/headers/curly.hpp/curly.hpp index 675b7fc..d2db43d 100644 --- a/headers/curly.hpp/curly.hpp +++ b/headers/curly.hpp/curly.hpp @@ -140,6 +140,9 @@ namespace curly_hpp bool cancel() noexcept; statuses status() const noexcept; + bool is_done() const noexcept; + bool is_pending() const noexcept; + statuses wait() const noexcept; statuses wait_for(time_ms_t ms) const noexcept; statuses wait_until(time_point_t tp) const noexcept; diff --git a/sources/curly.cpp b/sources/curly.cpp index 6debdbc..ed7aa24 100644 --- a/sources/curly.cpp +++ b/sources/curly.cpp @@ -509,6 +509,16 @@ namespace curly_hpp return status_; } + bool is_done() const noexcept { + std::lock_guard guard(mutex_); + return status_ == statuses::done; + } + + bool is_pending() const noexcept { + std::lock_guard guard(mutex_); + return status_ == statuses::pending; + } + statuses wait() const noexcept { std::unique_lock lock(mutex_); cvar_.wait(lock, [this](){ @@ -664,6 +674,14 @@ namespace curly_hpp return state_->status(); } + bool request::is_done() const noexcept { + return state_->is_done(); + } + + bool request::is_pending() const noexcept { + return state_->is_pending(); + } + request::statuses request::wait() const noexcept { return state_->wait(); } @@ -939,7 +957,7 @@ namespace curly_hpp curl_state::with([](CURLM* curlm){ for ( auto iter = active_handles.begin(); iter != active_handles.end(); ) { - if ( (*iter)->status() != request::statuses::pending ) { + if ( !(*iter)->is_pending() ) { (*iter)->dequeue(curlm); iter = active_handles.erase(iter); } else { diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index ae7868b..cd83181 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -109,6 +109,31 @@ TEST_CASE("curly") { } } + SECTION("is_done/is_pending") { + { + auto req = net::request_builder(net::methods::get) + .url("https://httpbin.org/delay/1") + .send(); + REQUIRE_FALSE(req.is_done()); + REQUIRE(req.is_pending()); + req.wait(); + REQUIRE(req.is_done()); + REQUIRE_FALSE(req.is_pending()); + } + { + auto req = net::request_builder(net::methods::post, "http://www.httpbin.org/post") + .url("https://httpbin.org/delay/2") + .request_timeout(net::time_sec_t(1)) + .send(); + REQUIRE_FALSE(req.is_done()); + REQUIRE(req.is_pending()); + req.wait(); + REQUIRE_FALSE(req.is_done()); + REQUIRE_FALSE(req.is_pending()); + REQUIRE(!req.get_error().empty()); + } + } + SECTION("get") { { auto req = net::request_builder("https://httpbin.org/status/204").send(); From ee00b426b27fa766925205e92565605d9012d0bf Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Sun, 30 Jun 2019 20:59:38 +0700 Subject: [PATCH 3/7] rename response_code to http_code --- README.md | 4 +-- headers/curly.hpp/curly.hpp | 8 ++--- sources/curly.cpp | 10 +++---- untests/curly_tests.cpp | 58 ++++++++++++++++++------------------- 4 files changed, 40 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 3e55ebc..05d118a 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ auto request = net::request_builder() auto response = request.get(); // prints results -std::cout << "Status code: " << response.code() << std::endl; +std::cout << "Status code: " << response.http_code() << std::endl; std::cout << "Content type: " << response.headers["content-type"] << std::endl; std::cout << "Body content: " << response.content.as_string_view() << std::endl; @@ -136,7 +136,7 @@ auto request = net::request_builder() if ( request.wait() == net::request::statuses::done ) { auto response = request.get(); - std::cout << "Status code: " << response.code() << std::endl; + std::cout << "Status code: " << response.http_code() << std::endl; } else { // throws net::exception because a response is unavailable // auto response = request.get(); diff --git a/headers/curly.hpp/curly.hpp b/headers/curly.hpp/curly.hpp index d2db43d..3be08eb 100644 --- a/headers/curly.hpp/curly.hpp +++ b/headers/curly.hpp/curly.hpp @@ -42,7 +42,7 @@ namespace curly_hpp post }; - using response_code_t = std::uint16_t; + using http_code_t = std::uint16_t; using headers_t = std::map; using time_sec_t = std::chrono::seconds; @@ -107,15 +107,15 @@ namespace curly_hpp response(const response&) = delete; response& operator=(const response&) = delete; - explicit response(response_code_t rc) noexcept; - response_code_t code() const noexcept; + explicit response(http_code_t c) noexcept; + http_code_t http_code() const noexcept; public: content_t content; headers_t headers; uploader_uptr uploader; downloader_uptr downloader; private: - response_code_t code_{0u}; + http_code_t http_code_{0u}; }; } diff --git a/sources/curly.cpp b/sources/curly.cpp index ed7aa24..4040675 100644 --- a/sources/curly.cpp +++ b/sources/curly.cpp @@ -297,11 +297,11 @@ namespace curly_hpp namespace curly_hpp { - response::response(response_code_t rc) noexcept - : code_(rc) {} + response::response(http_code_t c) noexcept + : http_code_(c) {} - response_code_t response::code() const noexcept { - return code_; + http_code_t response::http_code() const noexcept { + return http_code_; } } @@ -443,7 +443,7 @@ namespace curly_hpp } try { - response_ = response(static_cast(code)); + response_ = response(static_cast(code)); response_.content = std::move(response_content_); response_.headers = std::move(response_headers_); response_.uploader = std::move(breq_.uploader()); diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index cd83181..792879c 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -67,14 +67,14 @@ TEST_CASE("curly") { REQUIRE(req.wait() == net::request::statuses::done); REQUIRE(req.status() == net::request::statuses::done); auto resp = req.get(); - REQUIRE(resp.code() == 200u); + REQUIRE(resp.http_code() == 200u); REQUIRE(req.status() == net::request::statuses::empty); } { auto req = net::request_builder("https://httpbin.org/delay/2").send(); REQUIRE(req.wait_for(net::time_sec_t(1)) == net::request::statuses::pending); REQUIRE(req.wait_for(net::time_sec_t(5)) == net::request::statuses::done); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } { auto req = net::request_builder("https://httpbin.org/delay/2").send(); @@ -82,7 +82,7 @@ TEST_CASE("curly") { == net::request::statuses::pending); REQUIRE(req.wait_until(net::time_point_t::clock::now() + net::time_sec_t(5)) == net::request::statuses::done); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } } @@ -139,7 +139,7 @@ TEST_CASE("curly") { auto req = net::request_builder("https://httpbin.org/status/204").send(); auto resp = req.get(); REQUIRE(req.status() == net::request::statuses::empty); - REQUIRE(resp.code() == 204u); + REQUIRE(resp.http_code() == 204u); } { auto req = net::request_builder("https://httpbin.org/delay/2").send(); @@ -163,75 +163,75 @@ TEST_CASE("curly") { .url("https://httpbin.org/put") .method(net::methods::put) .send(); - REQUIRE(req0.get().code() == 200u); + REQUIRE(req0.get().http_code() == 200u); auto req1 = net::request_builder() .url("https://httpbin.org/put") .method(net::methods::get) .send(); - REQUIRE(req1.get().code() == 405u); + REQUIRE(req1.get().http_code() == 405u); auto req2 = net::request_builder() .url("https://httpbin.org/put") .method(net::methods::head) .send(); - REQUIRE(req2.get().code() == 405u); + REQUIRE(req2.get().http_code() == 405u); auto req3 = net::request_builder() .url("https://httpbin.org/put") .method(net::methods::post) .send(); - REQUIRE(req3.get().code() == 405u); + REQUIRE(req3.get().http_code() == 405u); } { auto req0 = net::request_builder() .url("https://httpbin.org/get") .method(net::methods::put) .send(); - REQUIRE(req0.get().code() == 405u); + REQUIRE(req0.get().http_code() == 405u); auto req1 = net::request_builder() .url("https://httpbin.org/get") .method(net::methods::get) .send(); - REQUIRE(req1.get().code() == 200u); + REQUIRE(req1.get().http_code() == 200u); auto req2 = net::request_builder() .url("https://httpbin.org/get") .method(net::methods::head) .send(); - REQUIRE(req2.get().code() == 200u); + REQUIRE(req2.get().http_code() == 200u); auto req3 = net::request_builder() .url("https://httpbin.org/get") .method(net::methods::post) .send(); - REQUIRE(req3.get().code() == 405u); + REQUIRE(req3.get().http_code() == 405u); } { auto req0 = net::request_builder() .url("https://httpbin.org/post") .method(net::methods::put) .send(); - REQUIRE(req0.get().code() == 405u); + REQUIRE(req0.get().http_code() == 405u); auto req1 = net::request_builder() .url("https://httpbin.org/post") .method(net::methods::get) .send(); - REQUIRE(req1.get().code() == 405u); + REQUIRE(req1.get().http_code() == 405u); auto req2 = net::request_builder() .url("https://httpbin.org/post") .method(net::methods::head) .send(); - REQUIRE(req2.get().code() == 405u); + REQUIRE(req2.get().http_code() == 405u); auto req3 = net::request_builder() .url("https://httpbin.org/post") .method(net::methods::post) .send(); - REQUIRE(req3.get().code() == 200u); + REQUIRE(req3.get().http_code() == 200u); } } @@ -241,28 +241,28 @@ TEST_CASE("curly") { .url("https://httpbin.org/status/200") .method(net::methods::put) .send(); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } { auto req = net::request_builder() .url("https://httpbin.org/status/201") .method(net::methods::get) .send(); - REQUIRE(req.get().code() == 201u); + REQUIRE(req.get().http_code() == 201u); } { auto req = net::request_builder() .url("https://httpbin.org/status/202") .method(net::methods::head) .send(); - REQUIRE(req.get().code() == 202u); + REQUIRE(req.get().http_code() == 202u); } { auto req = net::request_builder() .url("https://httpbin.org/status/203") .method(net::methods::post) .send(); - REQUIRE(req.get().code() == 203u); + REQUIRE(req.get().http_code() == 203u); } } @@ -350,7 +350,7 @@ TEST_CASE("curly") { .url("https://httpbin.org/image/png") .method(net::methods::get) .send().get(); - REQUIRE(resp.code() == 200u); + REQUIRE(resp.http_code() == 200u); REQUIRE(resp.headers.count("Content-Type")); REQUIRE(resp.headers.at("Content-Type") == "image/png"); REQUIRE(untests::png_data_length == resp.content.size()); @@ -363,7 +363,7 @@ TEST_CASE("curly") { .url("https://httpbin.org/image/jpeg") .method(net::methods::get) .send().get(); - REQUIRE(resp.code() == 200u); + REQUIRE(resp.http_code() == 200u); REQUIRE(resp.headers.count("Content-Type")); REQUIRE(resp.headers.at("Content-Type") == "image/jpeg"); REQUIRE(untests::jpeg_data_length == resp.content.size()); @@ -380,21 +380,21 @@ TEST_CASE("curly") { .url("https://httpbin.org/redirect/2") .method(net::methods::get) .send(); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } { auto req = net::request_builder() .url("https://httpbin.org/absolute-redirect/2") .method(net::methods::get) .send(); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } { auto req = net::request_builder() .url("https://httpbin.org/relative-redirect/2") .method(net::methods::get) .send(); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } } { @@ -404,7 +404,7 @@ TEST_CASE("curly") { .method(net::methods::get) .redirections(0) .send(); - REQUIRE(req.get().code() == 302u); + REQUIRE(req.get().http_code() == 302u); } { auto req = net::request_builder() @@ -428,7 +428,7 @@ TEST_CASE("curly") { .method(net::methods::get) .redirections(3) .send(); - REQUIRE(req.get().code() == 200u); + REQUIRE(req.get().http_code() == 200u); } } } @@ -554,7 +554,7 @@ TEST_CASE("curly_examples") { auto response = request.get(); // prints results - std::cout << "Status code: " << response.code() << std::endl; + std::cout << "Status code: " << response.http_code() << std::endl; std::cout << "Content type: " << response.headers["content-type"] << std::endl; std::cout << "Body content: " << response.content.as_string_view() << std::endl; @@ -612,7 +612,7 @@ TEST_CASE("curly_examples") { if ( request.wait() == net::request::statuses::done ) { auto response = request.get(); - std::cout << "Status code: " << response.code() << std::endl; + std::cout << "Status code: " << response.http_code() << std::endl; } else { // throws net::exception because a response is unavailable // auto response = request.get(); From e0f5be0fb3d1667116db09944c32d21a055da9ff Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Mon, 1 Jul 2019 01:53:18 +0700 Subject: [PATCH 4/7] first callback impl with tests --- README.md | 22 ++++++++- headers/curly.hpp/curly.hpp | 7 +++ sources/curly.cpp | 82 +++++++++++++++++++++++++------ untests/curly_tests.cpp | 97 ++++++++++++++++++++++++++++++++++++- 4 files changed, 191 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 05d118a..1953a42 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - Custom headers - Asynchronous requests - Different types of timeouts +- Custom completion callbacks - PUT, GET, HEAD, POST methods - Custom uploading and downloading streams @@ -134,7 +135,9 @@ auto request = net::request_builder() .url("http://unavailable.site.com") .send(); -if ( request.wait() == net::request::statuses::done ) { +request.wait(); + +if ( request.is_done() ) { auto response = request.get(); std::cout << "Status code: " << response.http_code() << std::endl; } else { @@ -147,6 +150,23 @@ if ( request.wait() == net::request::statuses::done ) { // Error message: Couldn't resolve host name ``` +### Request Callback + +```cpp +auto request = net::request_builder("http://www.httpbin.org/get") + .callback([](net::request request){ + if ( request.is_done() ) { + auto response = request.get(); + std::cout << "Status code: " << response.http_code() << std::endl; + } else { + std::cout << "Error message: " << request.get_error() << std::endl; + } + }).send(); + +request.wait_callback(); +// Status code: 200 +``` + ### Streamed Requests #### Downloading diff --git a/headers/curly.hpp/curly.hpp b/headers/curly.hpp/curly.hpp index 3be08eb..ca7d650 100644 --- a/headers/curly.hpp/curly.hpp +++ b/headers/curly.hpp/curly.hpp @@ -108,6 +108,8 @@ namespace curly_hpp response& operator=(const response&) = delete; explicit response(http_code_t c) noexcept; + + bool is_http_error() const noexcept; http_code_t http_code() const noexcept; public: content_t content; @@ -147,8 +149,13 @@ namespace curly_hpp statuses wait_for(time_ms_t ms) const noexcept; statuses wait_until(time_point_t tp) const noexcept; + statuses wait_callback() const noexcept; + statuses wait_callback_for(time_ms_t ms) const noexcept; + statuses wait_callback_until(time_point_t tp) const noexcept; + response get(); const std::string& get_error() const noexcept; + std::exception_ptr get_callback_exception() const noexcept; private: internal_state_ptr state_; }; diff --git a/sources/curly.cpp b/sources/curly.cpp index 4040675..32ed3c1 100644 --- a/sources/curly.cpp +++ b/sources/curly.cpp @@ -300,6 +300,10 @@ namespace curly_hpp response::response(http_code_t c) noexcept : http_code_(c) {} + bool response::is_http_error() const noexcept { + return http_code_ >= 400u; + } + http_code_t response::http_code() const noexcept { return http_code_; } @@ -431,11 +435,11 @@ namespace curly_hpp return false; } - long code = 0; + long http_code = 0; if ( CURLE_OK != curl_easy_getinfo( curlh_.get(), CURLINFO_RESPONSE_CODE, - &code) || !code ) + &http_code) || !http_code ) { status_ = statuses::failed; cvar_.notify_all(); @@ -443,7 +447,7 @@ namespace curly_hpp } try { - response_ = response(static_cast(code)); + response_ = response(static_cast(http_code)); response_.content = std::move(response_content_); response_.headers = std::move(response_headers_); response_.uploader = std::move(breq_.uploader()); @@ -519,26 +523,29 @@ namespace curly_hpp return status_ == statuses::pending; } - statuses wait() const noexcept { + statuses wait(bool wait_callback) const noexcept { std::unique_lock lock(mutex_); - cvar_.wait(lock, [this](){ - return status_ != statuses::pending; + cvar_.wait(lock, [this, wait_callback](){ + return (status_ != statuses::pending) + && (!wait_callback || callbacked_); }); return status_; } - statuses wait_for(time_ms_t ms) const noexcept { + statuses wait_for(time_ms_t ms, bool wait_callback) const noexcept { std::unique_lock lock(mutex_); - cvar_.wait_for(lock, ms, [this](){ - return status_ != statuses::pending; + cvar_.wait_for(lock, ms, [this, wait_callback](){ + return (status_ != statuses::pending) + && (!wait_callback || callbacked_); }); return status_; } - statuses wait_until(time_point_t tp) const noexcept { + statuses wait_until(time_point_t tp, bool wait_callback) const noexcept { std::unique_lock lock(mutex_); - cvar_.wait_until(lock, tp, [this](){ - return status_ != statuses::pending; + cvar_.wait_until(lock, tp, [this, wait_callback](){ + return (status_ != statuses::pending) + && (!wait_callback || callbacked_); }); return status_; } @@ -563,6 +570,30 @@ namespace curly_hpp return error_; } + std::exception_ptr get_callback_exception() const noexcept { + std::unique_lock lock(mutex_); + cvar_.wait(lock, [this](){ + return callbacked_; + }); + return callback_exception_; + } + + template < typename... Args > + void call_callback(Args&&... args) noexcept { + try { + if ( breq_.callback() ) { + breq_.callback()(std::forward(args)...); + } + } catch (...) { + std::lock_guard guard(mutex_); + callback_exception_ = std::current_exception(); + } + std::lock_guard guard(mutex_); + assert(!callbacked_ && status_ != statuses::pending); + callbacked_ = true; + cvar_.notify_all(); + } + bool check_response_timeout(time_point_t now) const noexcept { std::lock_guard guard(mutex_); return now - last_response_ >= response_timeout_; @@ -650,6 +681,9 @@ namespace curly_hpp private: std::atomic_size_t uploaded_{0u}; std::atomic_size_t downloaded_{0u}; + private: + bool callbacked_{false}; + std::exception_ptr callback_exception_{nullptr}; private: statuses status_{statuses::pending}; std::string error_{"Unknown error"}; @@ -683,15 +717,27 @@ namespace curly_hpp } request::statuses request::wait() const noexcept { - return state_->wait(); + return state_->wait(false); } request::statuses request::wait_for(time_ms_t ms) const noexcept { - return state_->wait_for(ms); + return state_->wait_for(ms, false); } request::statuses request::wait_until(time_point_t tp) const noexcept { - return state_->wait_until(tp); + return state_->wait_until(tp, false); + } + + request::statuses request::wait_callback() const noexcept { + return state_->wait(true); + } + + request::statuses request::wait_callback_for(time_ms_t ms) const noexcept { + return state_->wait_for(ms, true); + } + + request::statuses request::wait_callback_until(time_point_t tp) const noexcept { + return state_->wait_until(tp, true); } response request::get() { @@ -701,6 +747,10 @@ namespace curly_hpp const std::string& request::get_error() const noexcept { return state_->get_error(); } + + std::exception_ptr request::get_callback_exception() const noexcept { + return state_->get_callback_exception(); + } } // ----------------------------------------------------------------------------- @@ -917,6 +967,7 @@ namespace curly_hpp } catch (...) { sreq->fail(CURLcode::CURLE_FAILED_INIT); sreq->dequeue(curlm); + sreq->call_callback(sreq); } } }); @@ -959,6 +1010,7 @@ namespace curly_hpp for ( auto iter = active_handles.begin(); iter != active_handles.end(); ) { if ( !(*iter)->is_pending() ) { (*iter)->dequeue(curlm); + (*iter)->call_callback(*iter); iter = active_handles.erase(iter); } else { ++iter; diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index 792879c..c957159 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -538,6 +538,84 @@ TEST_CASE("curly") { REQUIRE(req.wait() == net::request::statuses::canceled); } } + + SECTION("callback") { + { + std::atomic_size_t call_once{0u}; + auto req = net::request_builder("http://www.httpbin.org/get") + .callback([&call_once](net::request request){ + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + ++call_once; + REQUIRE(request.is_done()); + REQUIRE(request.status() == net::request::statuses::done); + REQUIRE(request.get().http_code() == 200u); + }).send(); + REQUIRE(req.wait_callback() == net::request::statuses::empty); + REQUIRE_FALSE(req.get_callback_exception()); + REQUIRE(call_once.load() == 1u); + } + { + std::atomic_size_t call_once{0u}; + auto req = net::request_builder("|||") + .callback([&call_once](net::request request){ + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + ++call_once; + REQUIRE_FALSE(request.is_done()); + REQUIRE(request.status() == net::request::statuses::failed); + REQUIRE_FALSE(request.get_error().empty()); + }).send(); + REQUIRE(req.wait_callback() == net::request::statuses::failed); + REQUIRE_FALSE(req.get_callback_exception()); + REQUIRE(call_once.load() == 1u); + } + { + std::atomic_size_t call_once{0u}; + auto req = net::request_builder("http://www.httpbin.org/delay/2") + .response_timeout(net::time_sec_t(0)) + .callback([&call_once](net::request request){ + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + ++call_once; + REQUIRE_FALSE(request.is_done()); + REQUIRE(request.status() == net::request::statuses::timeout); + REQUIRE_FALSE(request.get_error().empty()); + }).send(); + REQUIRE(req.wait_callback() == net::request::statuses::timeout); + REQUIRE_FALSE(req.get_callback_exception()); + REQUIRE(call_once.load() == 1u); + } + { + std::atomic_size_t call_once{0u}; + auto req = net::request_builder("http://www.httpbin.org/delay/2") + .callback([&call_once](net::request request){ + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + ++call_once; + REQUIRE_FALSE(request.is_done()); + REQUIRE(request.status() == net::request::statuses::canceled); + REQUIRE(request.get_error().empty()); + }).send(); + REQUIRE(req.cancel()); + REQUIRE(req.wait_callback() == net::request::statuses::canceled); + REQUIRE_FALSE(req.get_callback_exception()); + REQUIRE(call_once.load() == 1u); + } + } + + SECTION("callback_exception") { + auto req = net::request_builder("http://www.httpbin.org/post") + .callback([](net::request request){ + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if ( request.get().is_http_error() ) { + throw std::logic_error("my_logic_error"); + } + }).send(); + REQUIRE(req.wait_callback() == net::request::statuses::empty); + REQUIRE(req.get_callback_exception()); + try { + std::rethrow_exception(req.get_callback_exception()); + } catch (const std::logic_error& e) { + REQUIRE(std::string_view("my_logic_error") == e.what()); + } + } } TEST_CASE("curly_examples") { @@ -610,7 +688,9 @@ TEST_CASE("curly_examples") { .url("http://unavailable.site.com") .send(); - if ( request.wait() == net::request::statuses::done ) { + request.wait(); + + if ( request.is_done() ) { auto response = request.get(); std::cout << "Status code: " << response.http_code() << std::endl; } else { @@ -623,6 +703,21 @@ TEST_CASE("curly_examples") { // Error message: Couldn't resolve host name } + SECTION("Request Callback") { + auto request = net::request_builder("http://www.httpbin.org/get") + .callback([](net::request request){ + if ( request.is_done() ) { + auto response = request.get(); + std::cout << "Status code: " << response.http_code() << std::endl; + } else { + std::cout << "Error message: " << request.get_error() << std::endl; + } + }).send(); + + request.wait_callback(); + // Status code: 200 + } + SECTION("Streamed Requests") { { class file_dowloader : public net::download_handler { From 84ebbe9928cbada1e679598c50fbb60aa8b58c18 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 2 Jul 2019 01:17:50 +0700 Subject: [PATCH 5/7] add promise.hpp to unit tests --- untests/CMakeLists.txt | 16 ++++++++++++++++ untests/curly_tests.cpp | 3 +++ 2 files changed, 19 insertions(+) diff --git a/untests/CMakeLists.txt b/untests/CMakeLists.txt index f2c526a..d6d767d 100644 --- a/untests/CMakeLists.txt +++ b/untests/CMakeLists.txt @@ -64,3 +64,19 @@ if(NOT tencent_rapidjson_POPULATED) target_include_directories(${PROJECT_NAME} PRIVATE ${tencent_rapidjson_SOURCE_DIR}/include) endif() + +# +# blackmatov/promise.hpp +# + +include(FetchContent) +FetchContent_Declare( + blackmatov_promise_hpp + GIT_REPOSITORY https://github.com/blackmatov/promise.hpp) + +FetchContent_GetProperties(blackmatov_promise_hpp) +if(NOT blackmatov_promise_hpp_POPULATED) + FetchContent_Populate(blackmatov_promise_hpp) + target_include_directories(${PROJECT_NAME} + PRIVATE ${blackmatov_promise_hpp_SOURCE_DIR}/headers) +endif() diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index c957159..dad6559 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -16,6 +16,9 @@ namespace json = rapidjson; #include namespace net = curly_hpp; +#include +namespace netex = promise_hpp; + #include "png_data.h" #include "jpeg_data.h" From 317cfe3bb9c39befc4315d002e1608c243aa0505 Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 2 Jul 2019 04:11:48 +0700 Subject: [PATCH 6/7] add promises example --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++- untests/curly_tests.cpp | 38 +++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1953a42..c1a25e4 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ if ( request.is_done() ) { // Error message: Couldn't resolve host name ``` -### Request Callback +### Request Callbacks ```cpp auto request = net::request_builder("http://www.httpbin.org/get") @@ -223,6 +223,52 @@ net::request_builder() .send().wait(); ``` +## Promised Requests + +Also, you can easily integrate promises like a [promise.hpp](https://github.com/BlackMATov/promise.hpp). + +```cpp +#include +namespace net = curly_hpp; + +#include +namespace netex = promise_hpp; + +netex::promise download(std::string url) { + return netex::make_promise([ + url = std::move(url) + ](auto resolve, auto reject){ + net::request_builder(std::move(url)) + .callback([resolve,reject](net::request request) mutable { + if ( !request.is_done() ) { + reject(net::exception("network error")); + return; + } + net::response response = request.get(); + if ( response.is_http_error() ) { + reject(net::exception("server error")); + return; + } + resolve(std::move(response.content)); + }).send(); + }); +} + +auto promise = download("https://httpbin.org/image/png") + .then([](const net::content_t& content){ + std::cout << content.size() << " bytes downloaded" << std::endl; + }).except([](std::exception_ptr e){ + try { + std::rethrow_exception(e); + } catch (const std::exception& ee) { + std::cerr << "Failed to download: " << ee.what() << std::endl; + } + }); + +promise.wait(); +// 8090 bytes downloaded +``` + ## API > coming soon! diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index dad6559..744e6ec 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -57,6 +57,26 @@ namespace throw std::exception(); } }; + + netex::promise download(std::string url) { + return netex::make_promise([ + url = std::move(url) + ](auto resolve, auto reject){ + net::request_builder(std::move(url)) + .callback([resolve,reject](net::request request) mutable { + if ( !request.is_done() ) { + reject(net::exception("network error")); + return; + } + net::response response = request.get(); + if ( response.is_http_error() ) { + reject(net::exception("server error")); + return; + } + resolve(std::move(response.content)); + }).send(); + }); + } } TEST_CASE("curly") { @@ -706,7 +726,7 @@ TEST_CASE("curly_examples") { // Error message: Couldn't resolve host name } - SECTION("Request Callback") { + SECTION("Request Callbacks") { auto request = net::request_builder("http://www.httpbin.org/get") .callback([](net::request request){ if ( request.is_done() ) { @@ -771,4 +791,20 @@ TEST_CASE("curly_examples") { .send().get(); } } + + SECTION("Promised Requests") { + auto promise = download("https://httpbin.org/image/png") + .then([](const net::content_t& content){ + std::cout << content.size() << " bytes downloaded" << std::endl; + }).except([](std::exception_ptr e){ + try { + std::rethrow_exception(e); + } catch (const std::exception& ee) { + std::cerr << "Failed to download: " << ee.what() << std::endl; + } + }); + + promise.wait(); + // 8090 bytes downloaded + } } From a61a73a620185dac1cad13db27c1c5fdd46050dc Mon Sep 17 00:00:00 2001 From: BlackMATov Date: Tue, 2 Jul 2019 04:27:26 +0700 Subject: [PATCH 7/7] fix last time of server response updating --- sources/curly.cpp | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/sources/curly.cpp b/sources/curly.cpp index 32ed3c1..0abece3 100644 --- a/sources/curly.cpp +++ b/sources/curly.cpp @@ -47,9 +47,8 @@ namespace public: using data_t = std::vector; - default_uploader(const data_t* src, std::mutex* m) noexcept + default_uploader(const data_t* src) noexcept : data_(*src) - , mutex_(*m) , size_(src->size()) {} std::size_t size() const override { @@ -57,7 +56,6 @@ namespace } std::size_t read(char* dst, std::size_t size) override { - std::lock_guard guard(mutex_); assert(size <= data_.size() - uploaded_); std::memcpy(dst, data_.data() + uploaded_, size); uploaded_ += size; @@ -65,7 +63,6 @@ namespace } private: const data_t& data_; - std::mutex& mutex_; std::size_t uploaded_{0}; const std::atomic_size_t size_{0}; }; @@ -74,18 +71,15 @@ namespace public: using data_t = std::vector; - default_downloader(data_t* dst, std::mutex* m) noexcept - : data_(*dst) - , mutex_(*m) {} + default_downloader(data_t* dst) noexcept + : data_(*dst) {} std::size_t write(const char* src, std::size_t size) override { - std::lock_guard guard(mutex_); data_.insert(data_.end(), src, src + size); return size; } private: data_t& data_; - std::mutex& mutex_; }; } @@ -323,11 +317,11 @@ namespace curly_hpp : breq_(std::move(rb)) { if ( !breq_.uploader() ) { - breq_.uploader(&breq_.content().data(), &mutex_); + breq_.uploader(&breq_.content().data()); } if ( !breq_.downloader() ) { - breq_.downloader(&response_content_, &mutex_); + breq_.downloader(&response_content_); } } @@ -620,16 +614,15 @@ namespace curly_hpp return self->header_callback_(buffer, size * nitems); } private: - void response_callback_() noexcept { - std::lock_guard guard(mutex_); - last_response_ = time_point_t::clock::now(); - } - std::size_t upload_callback_(char* dst, std::size_t size) noexcept { try { - size = std::min(size, breq_.uploader()->size() - uploaded_.load()); + std::lock_guard guard(mutex_); + last_response_ = time_point_t::clock::now(); + + size = std::min(size, breq_.uploader()->size() - uploaded_); const std::size_t read_bytes = breq_.uploader()->read(dst, size); - uploaded_.fetch_add(read_bytes); + uploaded_ += read_bytes; + return read_bytes; } catch (...) { return CURL_READFUNC_ABORT; @@ -638,8 +631,12 @@ namespace curly_hpp std::size_t download_callback_(const char* src, std::size_t size) noexcept { try { + std::lock_guard guard(mutex_); + last_response_ = time_point_t::clock::now(); + const std::size_t written_bytes = breq_.downloader()->write(src, size); - downloaded_.fetch_add(written_bytes); + downloaded_ += written_bytes; + return written_bytes; } catch (...) { return 0u; @@ -649,6 +646,8 @@ namespace curly_hpp std::size_t header_callback_(const char* src, std::size_t size) noexcept { try { std::lock_guard guard(mutex_); + last_response_ = time_point_t::clock::now(); + const std::string_view header(src, size); if ( !header.compare(0u, 5u, "HTTP/") ) { response_headers_.clear(); @@ -663,6 +662,7 @@ namespace curly_hpp response_headers_.emplace(key, val); } } + return header.size(); } catch (...) { return 0; @@ -679,8 +679,8 @@ namespace curly_hpp headers_t response_headers_; std::vector response_content_; private: - std::atomic_size_t uploaded_{0u}; - std::atomic_size_t downloaded_{0u}; + std::size_t uploaded_{0u}; + std::size_t downloaded_{0u}; private: bool callbacked_{false}; std::exception_ptr callback_exception_{nullptr};