diff --git a/headers/curly.hpp/curly.hpp b/headers/curly.hpp/curly.hpp index e6a7faa..5540a2b 100644 --- a/headers/curly.hpp/curly.hpp +++ b/headers/curly.hpp/curly.hpp @@ -26,6 +26,7 @@ #include #include #include +#include namespace curly_hpp { @@ -92,6 +93,17 @@ namespace curly_hpp { namespace detail { + struct case_string_compare final { + using is_transparent = void; + bool operator()(std::string_view l, std::string_view r) const noexcept { + return std::lexicographical_compare( + l.begin(), l.end(), r.begin(), r.end(), + [](const char lc, const char rc) noexcept { + return lc < rc; + }); + } + }; + struct icase_string_compare final { using is_transparent = void; bool operator()(std::string_view l, std::string_view r) const noexcept { @@ -104,9 +116,19 @@ namespace curly_hpp }; } + using qparams_t = std::multimap< + std::string, std::string, + detail::case_string_compare>; + using headers_t = std::map< std::string, std::string, detail::icase_string_compare>; + + using qparam_ilist_t = std::initializer_list< + std::pair>; + + using header_ilist_t = std::initializer_list< + std::pair>; } namespace curly_hpp @@ -249,7 +271,12 @@ namespace curly_hpp request_builder& url(std::string u) noexcept; request_builder& method(http_method m) noexcept; - request_builder& header(std::string key, std::string value); + + request_builder& qparams(qparam_ilist_t ps); + request_builder& qparam(std::string k, std::string v); + + request_builder& headers(header_ilist_t hs); + request_builder& header(std::string k, std::string v); request_builder& verbose(bool v) noexcept; request_builder& verification(bool v) noexcept; @@ -267,6 +294,7 @@ namespace curly_hpp const std::string& url() const noexcept; http_method method() const noexcept; + const qparams_t& qparams() const noexcept; const headers_t& headers() const noexcept; bool verbose() const noexcept; @@ -293,6 +321,24 @@ namespace curly_hpp request send(); + template < typename Iter > + request_builder& qparams(Iter first, Iter last) { + while ( first != last ) { + qparam(first->first, first->second); + ++first; + } + return *this; + } + + template < typename Iter > + request_builder& headers(Iter first, Iter last) { + while ( first != last ) { + header(first->first, first->second); + ++first; + } + return *this; + } + template < typename Callback > request_builder& callback(Callback&& f) { static_assert( @@ -327,6 +373,7 @@ namespace curly_hpp private: std::string url_; http_method method_{http_method::GET}; + qparams_t qparams_; headers_t headers_; bool verbose_{false}; bool verification_{false}; diff --git a/sources/curly.hpp/curly.cpp b/sources/curly.hpp/curly.cpp index 373a350..f6a1aa6 100644 --- a/sources/curly.hpp/curly.cpp +++ b/sources/curly.hpp/curly.cpp @@ -33,11 +33,11 @@ namespace using curlh_t = std::unique_ptr< CURL, - void(*)(CURL*)>; + decltype(&curl_easy_cleanup)>; using slist_t = std::unique_ptr< curl_slist, - void(*)(curl_slist*)>; + decltype(&curl_slist_free_all)>; class default_uploader final : public upload_handler { public: @@ -190,6 +190,36 @@ namespace } return {result, &curl_slist_free_all}; } + + std::string make_escaped_string(std::string_view s) { + std::unique_ptr escaped_string{ + curl_easy_escape(nullptr, s.data(), static_cast(s.size())), + &curl_free}; + if ( !escaped_string ) { + throw std::bad_alloc(); + } + return std::string(escaped_string.get()); + } + + std::string make_escaped_url(std::string_view u, const qparams_t& ps) { + std::string result{u}; + bool has_qparams = result.find('?') != std::string_view::npos; + for ( auto iter = ps.begin(); iter != ps.end(); ++iter ) { + const std::string k = !iter->first.empty() ? iter->first : iter->second; + const std::string v = !iter->first.empty() ? iter->second : std::string(); + if ( k.empty() ) { + continue; + } + result.append(has_qparams ? "&" : "?"); + result.append(make_escaped_string(k)); + if ( !v.empty() ) { + result.append("="); + result.append(make_escaped_string(v)); + } + has_qparams = true; + } + return result; + } } // ----------------------------------------------------------------------------- @@ -282,6 +312,7 @@ namespace curly_hpp } hlist_ = make_header_slist(breq_.headers()); + url_with_qparams_ = make_escaped_url(breq_.url(), breq_.qparams()); if ( const auto* vi = curl_version_info(CURLVERSION_NOW); vi && vi->version ) { std::string user_agent("cURL/"); @@ -308,7 +339,7 @@ namespace curly_hpp curl_easy_setopt(curlh_.get(), CURLOPT_HEADERDATA, this); curl_easy_setopt(curlh_.get(), CURLOPT_HEADERFUNCTION, &s_header_callback_); - curl_easy_setopt(curlh_.get(), CURLOPT_URL, breq_.url().c_str()); + curl_easy_setopt(curlh_.get(), CURLOPT_URL, url_with_qparams_.c_str()); curl_easy_setopt(curlh_.get(), CURLOPT_HTTPHEADER, hlist_.get()); curl_easy_setopt(curlh_.get(), CURLOPT_VERBOSE, breq_.verbose() ? 1l : 0l); @@ -685,6 +716,7 @@ namespace curly_hpp request_builder breq_; curlh_t curlh_{nullptr, &curl_easy_cleanup}; slist_t hlist_{nullptr, &curl_slist_free_all}; + std::string url_with_qparams_; time_point_t last_response_{time_point_t::clock::now()}; time_point_t::duration response_timeout_{0}; private: @@ -799,8 +831,27 @@ namespace curly_hpp return *this; } - request_builder& request_builder::header(std::string key, std::string value) { - headers_.insert_or_assign(std::move(key), std::move(value)); + request_builder& request_builder::qparams(qparam_ilist_t ps) { + for ( const auto& [k,v] : ps ) { + qparams_.emplace(k, v); + } + return *this; + } + + request_builder& request_builder::qparam(std::string k, std::string v) { + qparams_.emplace(std::move(k), std::move(v)); + return *this; + } + + request_builder& request_builder::headers(header_ilist_t hs) { + for ( const auto& [k,v] : hs ) { + headers_.insert_or_assign(std::string(k), v); + } + return *this; + } + + request_builder& request_builder::header(std::string k, std::string v) { + headers_.insert_or_assign(std::move(k), std::move(v)); return *this; } @@ -872,6 +923,10 @@ namespace curly_hpp return method_; } + const qparams_t& request_builder::qparams() const noexcept { + return qparams_; + } + const headers_t& request_builder::headers() const noexcept { return headers_; } diff --git a/untests/curly_tests.cpp b/untests/curly_tests.cpp index 7bcc594..55e1873 100644 --- a/untests/curly_tests.cpp +++ b/untests/curly_tests.cpp @@ -371,17 +371,58 @@ TEST_CASE("curly") { } SECTION("request_inspection") { - auto req = net::request_builder() - .url("https://httpbin.org/headers") - .header("Custom-Header-1", "custom_header_value_1") - .header("Custom-Header-2", "custom header value 2") - .header("Custom-Header-3", std::string()) - .send(); - const auto resp = req.take(); - const auto content_j = json_parse(resp.content.as_string_view()); - REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1"); - REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2"); - REQUIRE(content_j["headers"]["Custom-Header-3"] == ""); + { + auto resp = net::request_builder() + .url("https://httpbin.org/headers") + .header("Custom-Header-1", "custom_header_value_1") + .header("Custom-Header-2", "custom header value 2") + .header("Custom-Header-3", std::string()) + .send().take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1"); + REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2"); + REQUIRE(content_j["headers"]["Custom-Header-3"] == ""); + } + { + auto resp = net::request_builder() + .url("https://httpbin.org/headers") + .headers({ + {"Custom-Header-1", "custom_header_value_1"}, + {"Custom-Header-2", "custom header value 2"}, + {"Custom-Header-3", ""}}) + .send().take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1"); + REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2"); + REQUIRE(content_j["headers"]["Custom-Header-3"] == ""); + } + { + auto resp = net::request_builder() + .url("https://httpbin.org/headers") + .headers({ + {"Custom-Header-1", "custom_header_value_1"}, + {"Custom-Header-2", "custom header value 2"}, + {"Custom-Header-3", ""}}) + .send().take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1"); + REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2"); + REQUIRE(content_j["headers"]["Custom-Header-3"] == ""); + } + { + std::map headers{ + {"Custom-Header-1", "custom_header_value_1"}, + {"Custom-Header-2", "custom header value 2"}, + {"Custom-Header-3", ""}}; + auto resp = net::request_builder() + .url("https://httpbin.org/headers") + .headers(headers.begin(), headers.end()) + .send().take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["headers"]["Custom-Header-1"] == "custom_header_value_1"); + REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2"); + REQUIRE(content_j["headers"]["Custom-Header-3"] == ""); + } } SECTION("response_inspection") { @@ -397,14 +438,56 @@ TEST_CASE("curly") { } { auto req = net::request_builder() - .url("https://httpbin.org/response-headers?hello=world&world=hello") + .url("https://httpbin.org/response-headers?hello=world") .method(net::http_method::POST) + .qparam("world", "hello") .send(); const auto resp = req.take(); const auto content_j = json_parse(resp.content.as_string_copy()); REQUIRE(content_j["hello"] == "world"); REQUIRE(content_j["world"] == "hello"); } + { + auto req = net::request_builder() + .url("https://httpbin.org/response-headers") + .method(net::http_method::GET) + .qparam("hello", "world") + .qparam("world", "hello") + .send(); + const auto resp = req.take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["hello"] == "world"); + REQUIRE(content_j["world"] == "hello"); + } + { + auto req = net::request_builder() + .url("https://httpbin.org/response-headers") + .method(net::http_method::GET) + .qparams({ + {"", "hello"}, + {"world", ""} + }) + .send(); + const auto resp = req.take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["hello"] == ""); + REQUIRE(content_j["world"] == ""); + } + { + std::map qparams{ + {"hello", "world"}, + {"world", "hello"} + }; + auto req = net::request_builder() + .url("https://httpbin.org/response-headers") + .method(net::http_method::GET) + .qparams(qparams.begin(), qparams.end()) + .send(); + const auto resp = req.take(); + const auto content_j = json_parse(resp.content.as_string_view()); + REQUIRE(content_j["hello"] == "world"); + REQUIRE(content_j["world"] == "hello"); + } } SECTION("dynamic_data") {