add encoded query parameters #8

This commit is contained in:
2019-07-15 15:38:18 +07:00
parent 39cfa6ee1b
commit e47fab74a3
3 changed files with 203 additions and 18 deletions

View File

@@ -26,6 +26,7 @@
#include <map> #include <map>
#include <vector> #include <vector>
#include <string> #include <string>
#include <initializer_list>
namespace curly_hpp namespace curly_hpp
{ {
@@ -92,6 +93,17 @@ namespace curly_hpp
{ {
namespace detail 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 { struct icase_string_compare final {
using is_transparent = void; using is_transparent = void;
bool operator()(std::string_view l, std::string_view r) const noexcept { 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< using headers_t = std::map<
std::string, std::string, std::string, std::string,
detail::icase_string_compare>; detail::icase_string_compare>;
using qparam_ilist_t = std::initializer_list<
std::pair<std::string_view, std::string_view>>;
using header_ilist_t = std::initializer_list<
std::pair<std::string_view, std::string_view>>;
} }
namespace curly_hpp namespace curly_hpp
@@ -249,7 +271,12 @@ namespace curly_hpp
request_builder& url(std::string u) noexcept; request_builder& url(std::string u) noexcept;
request_builder& method(http_method m) 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& verbose(bool v) noexcept;
request_builder& verification(bool v) noexcept; request_builder& verification(bool v) noexcept;
@@ -267,6 +294,7 @@ namespace curly_hpp
const std::string& url() const noexcept; const std::string& url() const noexcept;
http_method method() const noexcept; http_method method() const noexcept;
const qparams_t& qparams() const noexcept;
const headers_t& headers() const noexcept; const headers_t& headers() const noexcept;
bool verbose() const noexcept; bool verbose() const noexcept;
@@ -293,6 +321,24 @@ namespace curly_hpp
request send(); 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 > template < typename Callback >
request_builder& callback(Callback&& f) { request_builder& callback(Callback&& f) {
static_assert( static_assert(
@@ -327,6 +373,7 @@ namespace curly_hpp
private: private:
std::string url_; std::string url_;
http_method method_{http_method::GET}; http_method method_{http_method::GET};
qparams_t qparams_;
headers_t headers_; headers_t headers_;
bool verbose_{false}; bool verbose_{false};
bool verification_{false}; bool verification_{false};

View File

@@ -33,11 +33,11 @@ namespace
using curlh_t = std::unique_ptr< using curlh_t = std::unique_ptr<
CURL, CURL,
void(*)(CURL*)>; decltype(&curl_easy_cleanup)>;
using slist_t = std::unique_ptr< using slist_t = std::unique_ptr<
curl_slist, curl_slist,
void(*)(curl_slist*)>; decltype(&curl_slist_free_all)>;
class default_uploader final : public upload_handler { class default_uploader final : public upload_handler {
public: public:
@@ -190,6 +190,36 @@ namespace
} }
return {result, &curl_slist_free_all}; return {result, &curl_slist_free_all};
} }
std::string make_escaped_string(std::string_view s) {
std::unique_ptr<char, decltype(&curl_free)> escaped_string{
curl_easy_escape(nullptr, s.data(), static_cast<int>(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()); 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 ) { if ( const auto* vi = curl_version_info(CURLVERSION_NOW); vi && vi->version ) {
std::string user_agent("cURL/"); 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_HEADERDATA, this);
curl_easy_setopt(curlh_.get(), CURLOPT_HEADERFUNCTION, &s_header_callback_); 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_HTTPHEADER, hlist_.get());
curl_easy_setopt(curlh_.get(), CURLOPT_VERBOSE, breq_.verbose() ? 1l : 0l); curl_easy_setopt(curlh_.get(), CURLOPT_VERBOSE, breq_.verbose() ? 1l : 0l);
@@ -685,6 +716,7 @@ namespace curly_hpp
request_builder breq_; request_builder breq_;
curlh_t curlh_{nullptr, &curl_easy_cleanup}; curlh_t curlh_{nullptr, &curl_easy_cleanup};
slist_t hlist_{nullptr, &curl_slist_free_all}; 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 last_response_{time_point_t::clock::now()};
time_point_t::duration response_timeout_{0}; time_point_t::duration response_timeout_{0};
private: private:
@@ -799,8 +831,27 @@ namespace curly_hpp
return *this; return *this;
} }
request_builder& request_builder::header(std::string key, std::string value) { request_builder& request_builder::qparams(qparam_ilist_t ps) {
headers_.insert_or_assign(std::move(key), std::move(value)); 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; return *this;
} }
@@ -872,6 +923,10 @@ namespace curly_hpp
return method_; return method_;
} }
const qparams_t& request_builder::qparams() const noexcept {
return qparams_;
}
const headers_t& request_builder::headers() const noexcept { const headers_t& request_builder::headers() const noexcept {
return headers_; return headers_;
} }

View File

@@ -371,17 +371,58 @@ TEST_CASE("curly") {
} }
SECTION("request_inspection") { SECTION("request_inspection") {
auto req = net::request_builder() {
.url("https://httpbin.org/headers") auto resp = net::request_builder()
.header("Custom-Header-1", "custom_header_value_1") .url("https://httpbin.org/headers")
.header("Custom-Header-2", "custom header value 2") .header("Custom-Header-1", "custom_header_value_1")
.header("Custom-Header-3", std::string()) .header("Custom-Header-2", "custom header value 2")
.send(); .header("Custom-Header-3", std::string())
const auto resp = req.take(); .send().take();
const auto content_j = json_parse(resp.content.as_string_view()); 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-1"] == "custom_header_value_1");
REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2"); REQUIRE(content_j["headers"]["Custom-Header-2"] == "custom header value 2");
REQUIRE(content_j["headers"]["Custom-Header-3"] == ""); 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<std::string, std::string> 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") { SECTION("response_inspection") {
@@ -397,14 +438,56 @@ TEST_CASE("curly") {
} }
{ {
auto req = net::request_builder() 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) .method(net::http_method::POST)
.qparam("world", "hello")
.send(); .send();
const auto resp = req.take(); const auto resp = req.take();
const auto content_j = json_parse(resp.content.as_string_copy()); const auto content_j = json_parse(resp.content.as_string_copy());
REQUIRE(content_j["hello"] == "world"); REQUIRE(content_j["hello"] == "world");
REQUIRE(content_j["world"] == "hello"); 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<std::string,std::string> 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") { SECTION("dynamic_data") {