mirror of
https://github.com/BlackMATov/curly.hpp.git
synced 2025-12-13 03:29:37 +07:00
add encoded query parameters #8
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <initializer_list>
|
||||
|
||||
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<std::string_view, std::string_view>>;
|
||||
|
||||
using header_ilist_t = std::initializer_list<
|
||||
std::pair<std::string_view, std::string_view>>;
|
||||
}
|
||||
|
||||
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};
|
||||
|
||||
@@ -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<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());
|
||||
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_;
|
||||
}
|
||||
|
||||
@@ -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<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") {
|
||||
@@ -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<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") {
|
||||
|
||||
Reference in New Issue
Block a user