This is an automated email from the ASF dual-hosted git repository.

wangdan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pegasus.git


The following commit(s) were added to refs/heads/master by this push:
     new c0fcba082 feat(http_client): add http_url class to help build URLs for 
http client (#1843)
c0fcba082 is described below

commit c0fcba082cd6665625dd6c42438c33c35e19d216
Author: Dan Wang <[email protected]>
AuthorDate: Fri Jan 19 14:16:23 2024 +0800

    feat(http_client): add http_url class to help build URLs for http client 
(#1843)
    
    There are multiple components for a URL used to issue http requests,
    such as scheme, host, port, path and query. However, to build each
    URL by such components, we could assemble them into a string only
    by fmt::format or concatenating them together with some separators.
    
    This PR is to provide a class named http_url that help build URLs on
    demand. Just specify each component we need to http_url, then
    extract the URL string. We could set the extracted the URL string,
    or just set http_url object itself to http client to tell what is the URL
    for the http requests.
---
 src/http/http_client.cpp           | 315 +++++++++++++++++++++++++++-----
 src/http/http_client.h             | 104 ++++++++++-
 src/http/test/http_client_test.cpp | 355 ++++++++++++++++++++++++++++++++++---
 src/utils/enum_helper.h            |   3 +-
 4 files changed, 704 insertions(+), 73 deletions(-)

diff --git a/src/http/http_client.cpp b/src/http/http_client.cpp
index dbd60494b..1af5ebb4e 100644
--- a/src/http/http_client.cpp
+++ b/src/http/http_client.cpp
@@ -34,6 +34,188 @@ DSN_DEFINE_uint32(http,
                   "The maximum time in milliseconds that you allow the libcurl 
transfer operation "
                   "to complete");
 
+#define RETURN_IF_CURL_NOT_OK(expr, ok, ...)                                   
                    \
+    do {                                                                       
                    \
+        const auto &code = (expr);                                             
                    \
+        if (dsn_unlikely(code != (ok))) {                                      
                    \
+            std::string msg(fmt::format("{}: {}", fmt::format(__VA_ARGS__), 
to_error_msg(code)));  \
+            return dsn::error_s::make(to_error_code(code), msg);               
                    \
+        }                                                                      
                    \
+    } while (0)
+
+#define CHECK_IF_CURL_OK(expr, ok, ...)                                        
                    \
+    do {                                                                       
                    \
+        const auto &code = (expr);                                             
                    \
+        CHECK_EXPRESSION(                                                      
                    \
+            #expr == #ok, code == (ok), "{}: {}", fmt::format(__VA_ARGS__), 
to_error_msg(code));   \
+    } while (0)
+
+#define CHECK_IF_CURL_URL_OK(url, expr, ...)                                   
                    \
+    do {                                                                       
                    \
+        CHECK_NOTNULL(url, "CURLU object has not been allocated");             
                    \
+        CHECK_IF_CURL_OK(expr, CURLUE_OK, __VA_ARGS__);                        
                    \
+    } while (0)
+
+#define CHECK_IF_CURL_URL_SET_OK(url, part, content, ...)                      
                    \
+    CHECK_IF_CURL_URL_OK(url, curl_url_set(url, CURLUPART_##part, content, 0), 
__VA_ARGS__)
+
+#define CHECK_IF_CURL_URL_SET_NULL_OK(url, part, ...)                          
                    \
+    CHECK_IF_CURL_URL_SET_OK(url,                                              
                    \
+                             part,                                             
                    \
+                             nullptr,                                          
                    \
+                             "curl_url_set(part = " #part                      
                    \
+                             ", content = nullptr) should always return 
CURLUE_OK")
+
+// Set "http" as the default scheme.
+#define SET_DEFAULT_HTTP_SCHEME(url)                                           
                    \
+    CHECK_IF_CURL_URL_SET_OK(url,                                              
                    \
+                             SCHEME,                                           
                    \
+                             enum_to_val(http_scheme::kHttp, 
std::string()).c_str(),               \
+                             "failed to set CURLUPART_SCHEME with 'http'")
+
+namespace {
+
+inline dsn::error_code to_error_code(CURLUcode code)
+{
+    switch (code) {
+    case CURLUE_OK:
+        return dsn::ERR_OK;
+    default:
+        return dsn::ERR_CURL_FAILED;
+    }
+}
+
+inline std::string to_error_msg(CURLUcode code)
+{
+    return fmt::format("code={}, desc=\"{}\"", static_cast<int>(code), 
curl_url_strerror(code));
+}
+
+CURLU *new_curlu()
+{
+    CURLU *url = curl_url();
+    CHECK_NOTNULL(url, "fail to allocate a CURLU object due to out of memory");
+
+    SET_DEFAULT_HTTP_SCHEME(url);
+
+    return url;
+}
+
+CURLU *dup_curlu(const CURLU *url)
+{
+    CURLU *new_url = curl_url_dup(url);
+    CHECK_NOTNULL(new_url, "fail to duplicate a CURLU object due to out of 
memory");
+
+    return new_url;
+}
+
+} // anonymous namespace
+
+void http_url::curlu_deleter::operator()(CURLU *url) const
+{
+    if (url == nullptr) {
+        return;
+    }
+
+    curl_url_cleanup(url);
+    url = nullptr;
+}
+
+http_url::http_url() noexcept : _url(new_curlu(), curlu_deleter()) {}
+
+http_url::http_url(const http_url &rhs) noexcept : 
_url(dup_curlu(rhs._url.get()), curlu_deleter())
+{
+}
+
+http_url &http_url::operator=(const http_url &rhs) noexcept
+{
+    if (this == &rhs) {
+        return *this;
+    }
+
+    // The existing deleter will not be cleared.
+    _url.reset(dup_curlu(rhs._url.get()));
+    return *this;
+}
+
+http_url::http_url(http_url &&rhs) noexcept : _url(std::move(rhs._url)) {}
+
+http_url &http_url::operator=(http_url &&rhs) noexcept
+{
+    if (this == &rhs) {
+        return *this;
+    }
+
+    _url = std::move(rhs._url);
+    return *this;
+}
+
+void http_url::clear()
+{
+    // Setting the url with nullptr would lead to the release of memory for 
each part of the url,
+    // thus clearing the url.
+    CHECK_IF_CURL_URL_SET_NULL_OK(_url.get(), URL);
+
+    SET_DEFAULT_HTTP_SCHEME(_url.get());
+}
+
+#define RETURN_IF_CURL_URL_NOT_OK(expr, ...) RETURN_IF_CURL_NOT_OK(expr, 
CURLUE_OK, __VA_ARGS__)
+
+#define RETURN_IF_CURL_URL_SET_NOT_OK(part, content, flags)                    
                    \
+    RETURN_IF_CURL_URL_NOT_OK(                                                 
                    \
+        curl_url_set(_url.get(), part, content, flags), "failed to set " #part 
" to {}", content)
+
+#define RETURN_IF_CURL_URL_GET_NOT_OK(part, content_pointer, flags)            
                    \
+    RETURN_IF_CURL_URL_NOT_OK(curl_url_get(_url.get(), part, content_pointer, 
flags),              \
+                              "failed to get " #part)
+
+#define DEF_HTTP_URL_SET_FUNC(name, part)                                      
                    \
+    dsn::error_s http_url::set_##name(const char *content)                     
                    \
+    {                                                                          
                    \
+        RETURN_IF_CURL_URL_SET_NOT_OK(CURLUPART_##part, content, 0);           
                    \
+                                                                               
                    \
+        return dsn::error_s::ok();                                             
                    \
+    }
+
+DEF_HTTP_URL_SET_FUNC(url, URL)
+
+DEF_HTTP_URL_SET_FUNC(scheme, SCHEME)
+
+dsn::error_s http_url::set_scheme(http_scheme scheme)
+{
+    return set_scheme(enum_to_val(scheme, std::string()).c_str());
+}
+
+DEF_HTTP_URL_SET_FUNC(host, HOST)
+
+DEF_HTTP_URL_SET_FUNC(port, PORT)
+
+dsn::error_s http_url::set_port(uint16_t port) { return 
set_port(std::to_string(port).c_str()); }
+
+DEF_HTTP_URL_SET_FUNC(path, PATH)
+
+DEF_HTTP_URL_SET_FUNC(query, QUERY)
+
+#undef DEF_HTTP_URL_SET_FUNC
+
+dsn::error_s http_url::to_string(std::string &url) const
+{
+    CHECK_NOTNULL(_url, "CURLU object has not been allocated");
+
+    char *content;
+    RETURN_IF_CURL_URL_GET_NOT_OK(CURLUPART_URL, &content, 0);
+
+    url = content;
+
+    // Free the returned string.
+    curl_free(content);
+
+    return dsn::error_s::ok();
+}
+
+#undef RETURN_IF_CURL_URL_GET_NOT_OK
+#undef RETURN_IF_CURL_URL_SET_NOT_OK
+#undef RETURN_IF_CURL_URL_NOT_OK
+
 http_client::http_client()
     : _curl(nullptr),
       _method(http_method::GET),
@@ -74,35 +256,46 @@ inline dsn::error_code to_error_code(CURLcode code)
 
 } // anonymous namespace
 
-#define RETURN_IF_CURL_NOT_OK(expr, ...)                                       
                    \
+#define RETURN_IF_CURL_EASY_NOT_OK(expr, ...)                                  
                    \
     do {                                                                       
                    \
-        const auto code = (expr);                                              
                    \
-        if (dsn_unlikely(code != CURLE_OK)) {                                  
                    \
-            std::string msg(fmt::format("{}: {}", fmt::format(__VA_ARGS__), 
to_error_msg(code)));  \
-            return dsn::error_s::make(to_error_code(code), msg);               
                    \
-        }                                                                      
                    \
+        CHECK_NOTNULL(_curl, "CURL object has not been allocated");            
                    \
+        RETURN_IF_CURL_NOT_OK(expr, CURLE_OK, __VA_ARGS__);                    
                    \
     } while (0)
 
-#define RETURN_IF_SETOPT_NOT_OK(opt, input)                                    
                    \
-    RETURN_IF_CURL_NOT_OK(curl_easy_setopt(_curl, opt, input),                 
                    \
-                          "failed to set " #opt " to"                          
                    \
-                          " " #input)
+#define RETURN_IF_CURL_EASY_SETOPT_NOT_OK(opt, input)                          
                    \
+    RETURN_IF_CURL_EASY_NOT_OK(curl_easy_setopt(_curl, CURLOPT_##opt, input),  
                    \
+                               "failed to set " #opt " with " #input)
 
-#define RETURN_IF_GETINFO_NOT_OK(info, output)                                 
                    \
-    RETURN_IF_CURL_NOT_OK(curl_easy_getinfo(_curl, info, output), "failed to 
get from " #info)
+#define RETURN_IF_CURL_EASY_GETINFO_NOT_OK(info, output)                       
                    \
+    RETURN_IF_CURL_EASY_NOT_OK(curl_easy_getinfo(_curl, CURLINFO_##info, 
output),                  \
+                               "failed to get from " #info)
 
-#define RETURN_IF_EXEC_METHOD_NOT_OK()                                         
                    \
-    RETURN_IF_CURL_NOT_OK(curl_easy_perform(_curl),                            
                    \
-                          "failed to perform http request(method={}, url={})", 
                    \
-                          enum_to_string(_method),                             
                    \
-                          _url)
+#define RETURN_IF_CURL_EASY_PERFORM_NOT_OK()                                   
                    \
+    RETURN_IF_CURL_EASY_NOT_OK(curl_easy_perform(_curl),                       
                    \
+                               "failed to perform http request(method={}, 
url={})",                \
+                               enum_to_string(_method),                        
                    \
+                               _url)
+
+#define CHECK_IF_CURL_EASY_OK(expr, ...)                                       
                    \
+    do {                                                                       
                    \
+        CHECK_NOTNULL(_curl, "CURL object has not been allocated");            
                    \
+        CHECK_IF_CURL_OK(expr, CURLE_OK, __VA_ARGS__);                         
                    \
+    } while (0)
+
+#define CHECK_IF_CURL_EASY_SETOPT_OK(opt, input, ...)                          
                    \
+    do {                                                                       
                    \
+        CHECK_NOTNULL(_curl, "CURL object has not been allocated");            
                    \
+        CHECK_IF_CURL_EASY_OK(curl_easy_setopt(_curl, CURLOPT_##opt, input),   
                    \
+                              "failed to set " #opt " with " #input ": {}",    
                    \
+                              fmt::format(__VA_ARGS__));                       
                    \
+    } while (0)
 
 dsn::error_s http_client::init()
 {
     if (_curl == nullptr) {
         _curl = curl_easy_init();
         if (_curl == nullptr) {
-            return dsn::error_s::make(dsn::ERR_CURL_FAILED, "fail to 
initialize curl");
+            return dsn::error_s::make(dsn::ERR_CURL_FAILED, "fail to allocate 
CURL object");
         }
     } else {
         curl_easy_reset(_curl);
@@ -113,20 +306,20 @@ dsn::error_s http_client::init()
 
     // Additional messages for errors are needed.
     clear_error_buf();
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_ERRORBUFFER, _error_buf);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(ERRORBUFFER, _error_buf);
 
     // Set with NOSIGNAL since we are multi-threaded.
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_NOSIGNAL, 1L);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(NOSIGNAL, 1L);
 
     // Redirects are supported.
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_FOLLOWLOCATION, 1L);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(FOLLOWLOCATION, 1L);
 
     // Before 8.3.0, CURLOPT_MAXREDIRS was unlimited.
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_MAXREDIRS, 20);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(MAXREDIRS, 20);
 
     // Set common timeout for transfer operation. Users could also change it 
with their
     // custom values by `set_timeout`.
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_TIMEOUT_MS, 
static_cast<long>(FLAGS_curl_timeout_ms));
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(TIMEOUT_MS, 
static_cast<long>(FLAGS_curl_timeout_ms));
 
     // A lambda can only be converted to a function pointer if it does not 
capture:
     // 
https://stackoverflow.com/questions/28746744/passing-capturing-lambda-as-function-pointer
@@ -135,10 +328,10 @@ dsn::error_s http_client::init()
         http_client *client = reinterpret_cast<http_client *>(param);
         return client->on_response_data(buffer, size * nmemb);
     };
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_WRITEFUNCTION, callback);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(WRITEFUNCTION, callback);
 
     // This http_client object itself is passed to the callback function.
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_WRITEDATA, reinterpret_cast<void *>(this));
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(WRITEDATA, reinterpret_cast<void 
*>(this));
 
     return dsn::error_s::ok();
 }
@@ -182,21 +375,50 @@ size_t http_client::on_response_data(const void *data, 
size_t length)
     return (*_recv_callback)(data, length) ? length : 
std::numeric_limits<size_t>::max();
 }
 
-dsn::error_s http_client::set_url(const std::string &url)
+#define CHECK_IF_CURL_EASY_SETOPT_CURLU_OK()                                   
                    \
+    CHECK_IF_CURL_EASY_SETOPT_OK(                                              
                    \
+        CURLU,                                                                 
                    \
+        _url.curlu(),                                                          
                    \
+        "once CURLOPT_CURLU is supported, curl_easy_setopt should always "     
                    \
+        "return CURLE_OK, see https://curl.se/libcurl/c/CURLOPT_CURLU.html";)
+
+dsn::error_s http_client::set_url(const std::string &new_url)
 {
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_URL, url.c_str());
+    // DO NOT call curl_easy_setopt() on CURLOPT_URL, since the CURLOPT_URL 
string is ignored
+    // if CURLOPT_CURLU is set. See following docs for details:
+    // * https://curl.se/libcurl/c/CURLOPT_CURLU.html
+    // * https://curl.se/libcurl/c/CURLOPT_URL.html
+    //
+    // Use a temporary object for the reason that once the error occurred, 
`_url` would not
+    // be dirty.
+    http_url tmp;
+    RETURN_NOT_OK(tmp.set_url(new_url.c_str()));
 
-    _url = url;
+    set_url(std::move(tmp));
     return dsn::error_s::ok();
 }
 
+void http_client::set_url(const http_url &new_url)
+{
+    _url = new_url;
+    CHECK_IF_CURL_EASY_SETOPT_CURLU_OK();
+}
+
+void http_client::set_url(http_url &&new_url)
+{
+    _url = std::move(new_url);
+    CHECK_IF_CURL_EASY_SETOPT_CURLU_OK();
+}
+
 dsn::error_s http_client::with_post_method(const std::string &data)
 {
-    // No need to enable CURLOPT_POST by 
`RETURN_IF_SETOPT_NOT_OK(CURLOPT_POST, 1L)`, since using
-    // either of CURLOPT_POSTFIELDS or CURLOPT_COPYPOSTFIELDS implies setting 
CURLOPT_POST to 1.
+    // No need to enable CURLOPT_POST by 
`RETURN_IF_CURL_EASY_SETOPT_NOT_OK(POST, 1L)`,
+    // since using either of CURLOPT_POSTFIELDS or CURLOPT_COPYPOSTFIELDS 
implies setting
+    // CURLOPT_POST to 1.
+    //
     // See https://curl.se/libcurl/c/CURLOPT_POSTFIELDS.html for details.
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_POSTFIELDSIZE, 
static_cast<long>(data.size()));
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_COPYPOSTFIELDS, data.data());
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(POSTFIELDSIZE, 
static_cast<long>(data.size()));
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(COPYPOSTFIELDS, data.data());
     _method = http_method::POST;
     return dsn::error_s::ok();
 }
@@ -209,7 +431,7 @@ dsn::error_s http_client::set_method(http_method method)
     // `with_post_method`.
     switch (method) {
     case http_method::GET:
-        RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPGET, 1L);
+        RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPGET, 1L);
         break;
     default:
         LOG_FATAL("Unsupported http_method");
@@ -219,22 +441,22 @@ dsn::error_s http_client::set_method(http_method method)
     return dsn::error_s::ok();
 }
 
-dsn::error_s http_client::set_auth(http_auth_type authType)
+dsn::error_s http_client::set_auth(http_auth_type auth_type)
 {
-    switch (authType) {
+    switch (auth_type) {
     case http_auth_type::SPNEGO:
-        RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_NEGOTIATE);
+        RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_NEGOTIATE);
         break;
     case http_auth_type::DIGEST:
-        RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
+        RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_DIGEST);
         break;
     case http_auth_type::BASIC:
-        RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
+        RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_BASIC);
         break;
     case http_auth_type::NONE:
         break;
     default:
-        RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPAUTH, CURLAUTH_ANY);
+        RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPAUTH, CURLAUTH_ANY);
         break;
     }
 
@@ -243,7 +465,7 @@ dsn::error_s http_client::set_auth(http_auth_type authType)
 
 dsn::error_s http_client::set_timeout(long timeout_ms)
 {
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_TIMEOUT_MS, timeout_ms);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(TIMEOUT_MS, timeout_ms);
     return dsn::error_s::ok();
 }
 
@@ -299,7 +521,7 @@ dsn::error_s http_client::process_header()
 
     // This would work well even if `_header_list` is NULL pointer. Pass a 
NULL to this option
     // to reset back to no custom headers. 
(https://curl.se/libcurl/c/CURLOPT_HTTPHEADER.html)
-    RETURN_IF_SETOPT_NOT_OK(CURLOPT_HTTPHEADER, _header_list);
+    RETURN_IF_CURL_EASY_SETOPT_NOT_OK(HTTPHEADER, _header_list);
 
     // New header has been built successfully, thus mark it unchanged.
     _header_changed = false;
@@ -315,7 +537,7 @@ dsn::error_s http_client::exec_method(const 
http_client::recv_callback &callback
 
     RETURN_NOT_OK(process_header());
 
-    RETURN_IF_EXEC_METHOD_NOT_OK();
+    RETURN_IF_CURL_EASY_PERFORM_NOT_OK();
     return dsn::error_s::ok();
 }
 
@@ -336,13 +558,16 @@ dsn::error_s http_client::exec_method(std::string 
*response)
 dsn::error_s http_client::get_http_status(http_status_code &status_code) const
 {
     long response_code;
-    RETURN_IF_GETINFO_NOT_OK(CURLINFO_RESPONSE_CODE, &response_code);
+    RETURN_IF_CURL_EASY_GETINFO_NOT_OK(RESPONSE_CODE, &response_code);
     status_code = enum_from_val(response_code, http_status_code::kInvalidCode);
     return dsn::error_s::ok();
 }
 
-#undef RETURN_IF_EXEC_METHOD_NOT_OK
-#undef RETURN_IF_SETOPT_NOT_OK
+#undef RETURN_IF_CURL_EASY_PERFORM_NOT_OK
+#undef RETURN_IF_CURL_EASY_GETINFO_NOT_OK
+#undef RETURN_IF_CURL_EASY_SETOPT_NOT_OK
+#undef RETURN_IF_CURL_EASY_NOT_OK
+
 #undef RETURN_IF_CURL_NOT_OK
 
 } // namespace dsn
diff --git a/src/http/http_client.h b/src/http/http_client.h
index e5a36802d..6596548e8 100644
--- a/src/http/http_client.h
+++ b/src/http/http_client.h
@@ -19,18 +19,101 @@
 
 #include <curl/curl.h>
 #include <stddef.h>
+#include <stdint.h>
 #include <functional>
+#include <iosfwd>
+#include <memory>
 #include <string>
 #include <unordered_map>
 
+#include "absl/strings/string_view.h"
 #include "http/http_method.h"
 #include "http/http_status_code.h"
+#include "utils/enum_helper.h"
 #include "utils/errors.h"
+#include "utils/fmt_utils.h"
 #include "utils/ports.h"
-#include "absl/strings/string_view.h"
+
+USER_DEFINED_ENUM_FORMATTER(CURLUcode)
+USER_DEFINED_ENUM_FORMATTER(CURLcode)
 
 namespace dsn {
 
+// Now https and ftp have not been supported. To support them, build curl by
+// "./configure --with-ssl=... --enable-ftp ...".
+#define ENUM_FOREACH_HTTP_SCHEME(DEF)                                          
                    \
+    DEF(Http, "http", http_scheme)                                             
                    \
+    DEF(Https, "https", http_scheme)                                           
                    \
+    DEF(Ftp, "ftp", http_scheme)
+
+enum class http_scheme : size_t
+{
+    ENUM_FOREACH_HTTP_SCHEME(ENUM_CONST_DEF) kCount,
+    kInvalidScheme,
+};
+
+ENUM_CONST_DEF_FROM_VAL_FUNC(std::string, http_scheme, 
ENUM_FOREACH_HTTP_SCHEME)
+ENUM_CONST_DEF_TO_VAL_FUNC(std::string, http_scheme, ENUM_FOREACH_HTTP_SCHEME)
+
+// A class that helps http client build URLs, based on CURLU object of libcurl.
+// About CURLU object, please see: https://curl.se/libcurl/c/libcurl-url.html.
+// About the usage, please see the comments for `http_client`.
+class http_url
+{
+public:
+    // The scheme is set to `http` as default for the URL.
+    http_url() noexcept;
+
+    ~http_url() = default;
+
+    http_url(const http_url &) noexcept;
+    http_url &operator=(const http_url &) noexcept;
+
+    http_url(http_url &&) noexcept;
+    http_url &operator=(http_url &&) noexcept;
+
+    // Clear the URL. And the scheme is reset to `http`.
+    void clear();
+
+    // Operations that update the components of a URL.
+    dsn::error_s set_url(const char *url);
+    dsn::error_s set_scheme(const char *scheme);
+    dsn::error_s set_scheme(http_scheme scheme);
+    dsn::error_s set_host(const char *host);
+    dsn::error_s set_port(const char *port);
+    dsn::error_s set_port(uint16_t port);
+    dsn::error_s set_path(const char *path);
+    dsn::error_s set_query(const char *query);
+
+    // Extract the URL string.
+    dsn::error_s to_string(std::string &url) const;
+
+    // Formatter for fmt::format.
+    friend std::ostream &operator<<(std::ostream &os, const http_url &url)
+    {
+        std::string str;
+        const auto &err = url.to_string(str);
+        if (dsn_unlikely(!err)) {
+            return os << err;
+        }
+        return os << str;
+    }
+
+private:
+    friend class http_client;
+    friend void test_after_move(http_url &, http_url &);
+
+    // Only used by `http_client` to get the underlying CURLU object.
+    CURLU *curlu() const { return _url.get(); }
+
+    struct curlu_deleter
+    {
+        void operator()(CURLU *url) const;
+    };
+
+    std::unique_ptr<CURLU, curlu_deleter> _url;
+};
+
 // A library for http client that provides convenient APIs to access http 
services, implemented
 // based on libcurl (https://curl.se/libcurl/c/).
 //
@@ -47,6 +130,13 @@ namespace dsn {
 // Specify the target url that you would request for:
 // err = client.set_url("http://<ip>:<port>/your/path");
 //
+// Or, you could use `http_url` to manage your URLs, and attach it to http 
client:
+// http_url url;
+// url_err = url.set_host(host);
+// url_err = url.set_port(port);
+// url_err = url.set_path(path);
+// err = client.set_url(url); // Or err = client.set_url(std::move(url));
+//
 // If you would use GET method, call `with_get_method`:
 // err = client.with_get_method();
 //
@@ -83,7 +173,11 @@ public:
     dsn::error_s init();
 
     // Specify the target url that the request would be sent for.
-    dsn::error_s set_url(const std::string &url);
+    dsn::error_s set_url(const std::string &new_url);
+
+    // Specify the target url by `http_url` class.
+    void set_url(const http_url &new_url);
+    void set_url(http_url &&new_url);
 
     // Using post method, with `data` as the payload for post body.
     dsn::error_s with_post_method(const std::string &data);
@@ -95,7 +189,7 @@ public:
     dsn::error_s set_timeout(long timeout_ms);
 
     // Specify the http auth type which include NONE BASIC DIGEST SPNEGO
-    dsn::error_s set_auth(http_auth_type authType);
+    dsn::error_s set_auth(http_auth_type auth_type);
 
     // Operations for the header fields.
     void clear_header_fields();
@@ -144,7 +238,7 @@ private:
 
     CURL *_curl;
     http_method _method;
-    std::string _url;
+    http_url _url;
     const recv_callback *_recv_callback;
     char _error_buf[kErrorBufferBytes];
 
@@ -156,3 +250,5 @@ private:
 };
 
 } // namespace dsn
+
+USER_DEFINED_STRUCTURE_FORMATTER(::dsn::http_url);
diff --git a/src/http/test/http_client_test.cpp 
b/src/http/test/http_client_test.cpp
index 0092e8dbf..95ecb9530 100644
--- a/src/http/test/http_client_test.cpp
+++ b/src/http/test/http_client_test.cpp
@@ -16,15 +16,17 @@
 // under the License.
 
 #include <fmt/core.h>
+#include <stdint.h>
 // IWYU pragma: no_include <gtest/gtest-message.h>
 // IWYU pragma: no_include <gtest/gtest-param-test.h>
 // IWYU pragma: no_include <gtest/gtest-test-part.h>
 #include <cstring>
 #include <iostream>
 #include <string>
-#include <tuple>
+#include <utility>
 #include <vector>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "http/http_client.h"
 #include "http/http_method.h"
@@ -32,8 +34,12 @@
 #include "utils/error_code.h"
 #include "utils/errors.h"
 #include "utils/fmt_logging.h"
+#include "utils/strings.h"
 #include "utils/test_macros.h"
 
+// IWYU pragma: no_forward_declare 
dsn::HttpClientMethodTest_ExecMethodByCopyUrlClass_Test
+// IWYU pragma: no_forward_declare 
dsn::HttpClientMethodTest_ExecMethodByMoveUrlClass_Test
+// IWYU pragma: no_forward_declare 
dsn::HttpClientMethodTest_ExecMethodByUrlString_Test
 namespace dsn {
 
 void check_expected_description_prefix(const std::string 
&expected_description_prefix,
@@ -42,11 +48,263 @@ void check_expected_description_prefix(const std::string 
&expected_description_p
     const std::string actual_description(err.description());
     std::cout << actual_description << std::endl;
 
-    ASSERT_LT(expected_description_prefix.size(), actual_description.size());
+    ASSERT_LE(expected_description_prefix.size(), actual_description.size());
     EXPECT_EQ(expected_description_prefix,
               actual_description.substr(0, 
expected_description_prefix.size()));
 }
 
+const std::string kTestUrlA("http://192.168.1.2/test/api0?key0=val0";);
+const std::string kTestUrlB("http://10.10.1.2/test/api1?key1=val1";);
+const std::string kTestUrlC("http://172.16.1.2/test/api2?key2=val2";);
+
+void set_url_string(http_url &url, const std::string &str)
+{
+    ASSERT_TRUE(url.set_url(str.c_str()));
+}
+
+void check_url_eq(const http_url &url, const std::string &expected_str)
+{
+    std::string actual_str;
+    ASSERT_TRUE(url.to_string(actual_str));
+    EXPECT_EQ(expected_str, actual_str);
+}
+
+void init_test_url(http_url &url)
+{
+    // Set original url with kTestUrlA before copy.
+    set_url_string(url, kTestUrlA);
+    check_url_eq(url, kTestUrlA);
+}
+
+void test_after_copy(http_url &url_1, http_url &url_2)
+{
+    // Check if both urls are expected immediately after copy.
+    check_url_eq(url_1, kTestUrlA);
+    check_url_eq(url_2, kTestUrlA);
+
+    // Set both urls with values other than kTestUrlA.
+    set_url_string(url_2, kTestUrlB);
+    check_url_eq(url_1, kTestUrlA);
+    check_url_eq(url_2, kTestUrlB);
+
+    set_url_string(url_1, kTestUrlC);
+    check_url_eq(url_1, kTestUrlC);
+    check_url_eq(url_2, kTestUrlB);
+}
+
+TEST(HttpUrlTest, CallCopyConstructor)
+{
+    http_url url_1;
+    init_test_url(url_1);
+
+    http_url url_2(url_1);
+    test_after_copy(url_1, url_2);
+}
+
+TEST(HttpUrlTest, CallCopyAssignmentOperator)
+{
+    http_url url_1;
+    init_test_url(url_1);
+
+    http_url url_2;
+    url_2 = url_1;
+    test_after_copy(url_1, url_2);
+}
+
+void test_after_move(http_url &url_1, http_url &url_2)
+{
+    // Check if both urls are expected immediately after move.
+    EXPECT_THAT(url_1._url, testing::IsNull());
+    check_url_eq(url_2, kTestUrlA);
+
+    // Set another url with value other than kTestUrlA.
+    set_url_string(url_2, kTestUrlB);
+    EXPECT_THAT(url_1._url, testing::IsNull());
+    check_url_eq(url_2, kTestUrlB);
+}
+
+TEST(HttpUrlTest, CallMoveConstructor)
+{
+    http_url url_1;
+    init_test_url(url_1);
+
+    http_url url_2(std::move(url_1));
+    test_after_move(url_1, url_2);
+}
+
+TEST(HttpUrlTest, CallMoveAssignmentOperator)
+{
+    http_url url_1;
+    init_test_url(url_1);
+
+    http_url url_2;
+    url_2 = std::move(url_1);
+    test_after_move(url_1, url_2);
+}
+
+const std::string kTestHost("192.168.1.2");
+const std::string kTestHttpUrl(fmt::format("http://{}/";, kTestHost));
+
+struct http_scheme_case
+{
+    http_scheme scheme;
+    error_code expected_err_code;
+    std::string expected_description_prefix;
+    std::string expected_str;
+};
+
+class HttpSchemeTest : public testing::TestWithParam<http_scheme_case>
+{
+protected:
+    void test_scheme(const http_scheme_case &scheme_case)
+    {
+        const auto &actual_err = _url.set_scheme(scheme_case.scheme);
+        ASSERT_EQ(scheme_case.expected_err_code, actual_err.code());
+        if (!actual_err) {
+            
NO_FATALS(check_expected_description_prefix(scheme_case.expected_description_prefix,
+                                                        actual_err));
+            return;
+        }
+
+        ASSERT_TRUE(_url.set_host(kTestHost.c_str()));
+
+        std::string actual_str;
+        ASSERT_TRUE(_url.to_string(actual_str));
+        EXPECT_EQ(scheme_case.expected_str, actual_str);
+    }
+
+private:
+    http_url _url;
+};
+
+TEST_P(HttpSchemeTest, SetScheme) { test_scheme(GetParam()); }
+
+const std::vector<http_scheme_case> http_scheme_tests = {
+    {http_scheme::kHttp, ERR_OK, "", kTestHttpUrl},
+    {http_scheme::kHttps,
+     ERR_CURL_FAILED,
+     "ERR_CURL_FAILED: failed to set CURLUPART_SCHEME to https: code=5, 
desc=\"Unsupported URL "
+     "scheme\"",
+     ""},
+    {http_scheme::kFtp,
+     ERR_CURL_FAILED,
+     "ERR_CURL_FAILED: failed to set CURLUPART_SCHEME to ftp: code=5, 
desc=\"Unsupported URL "
+     "scheme\"",
+     ""},
+};
+
+INSTANTIATE_TEST_CASE_P(HttpClientTest, HttpSchemeTest, 
testing::ValuesIn(http_scheme_tests));
+
+TEST(HttpUrlTest, Clear)
+{
+    http_url url;
+    url.set_url(kTestUrlA.c_str());
+
+    std::string str;
+    ASSERT_TRUE(url.to_string(str));
+    ASSERT_EQ(kTestUrlA, str);
+
+    url.clear();
+
+    // After cleared, at least it should be set with some host; otherwise, 
extracting the URL
+    // string would lead to error.
+    const auto &err = url.to_string(str);
+    ASSERT_FALSE(err);
+    const std::string expected_description_prefix(
+        "ERR_CURL_FAILED: failed to get CURLUPART_URL: code=14, desc=\"No host 
part in the URL\"");
+    NO_FATALS(check_expected_description_prefix(expected_description_prefix, 
err));
+
+    ASSERT_TRUE(url.set_host(kTestHost.c_str()));
+    ASSERT_TRUE(url.to_string(str));
+    ASSERT_EQ(kTestHttpUrl, str);
+}
+
+struct http_url_build_case
+{
+    const char *scheme;
+    const char *host;
+    uint16_t port;
+    const char *path;
+    const char *query;
+    const char *expected_url;
+};
+
+class HttpUrlBuildTest : public testing::TestWithParam<http_url_build_case>
+{
+protected:
+    void test_build_url(const http_url_build_case &build_case)
+    {
+        if (!utils::is_empty(build_case.scheme)) {
+            // Empty scheme will lead to error.
+            ASSERT_TRUE(_url.set_scheme(build_case.scheme));
+        }
+
+        ASSERT_TRUE(_url.set_host(build_case.host));
+        ASSERT_TRUE(_url.set_port(build_case.port));
+        ASSERT_TRUE(_url.set_path(build_case.path));
+        ASSERT_TRUE(_url.set_query(build_case.query));
+
+        std::string actual_url;
+        ASSERT_TRUE(_url.to_string(actual_url));
+        EXPECT_STREQ(build_case.expected_url, actual_url.c_str());
+    }
+
+private:
+    http_url _url;
+};
+
+TEST_P(HttpUrlBuildTest, BuildUrl) { test_build_url(GetParam()); }
+
+const std::vector<http_url_build_case> http_url_tests = {
+    // Test default scheme, specified ip, empty path and query.
+    {nullptr, "10.10.1.2", 34801, "", "", "http://10.10.1.2:34801/"},
+    // Test default scheme, specified host, empty path and query.
+    {nullptr, "www.example.com", 8080, "", "", "http://www.example.com:8080/"},
+    // Test default scheme, specified ip and path, empty query.
+    {nullptr, "10.10.1.2", 34801, "/api", "", "http://10.10.1.2:34801/api"},
+    // Test default scheme, specified host and path, empty query.
+    {nullptr, "www.example.com", 8080, "/api", "", 
"http://www.example.com:8080/api"},
+    // Test default scheme, specified ip, path and query.
+    {nullptr, "10.10.1.2", 34801, "/api", "foo=bar", 
"http://10.10.1.2:34801/api?foo=bar"},
+    // Test default scheme, specified ip, path and query.
+    {nullptr,
+     "www.example.com",
+     8080,
+     "/api",
+     "foo=bar",
+     "http://www.example.com:8080/api?foo=bar"},
+    // Test default scheme, specified ip, path and query with multiple keys.
+    {nullptr,
+     "10.10.1.2",
+     34801,
+     "/api",
+     "key1=abc&key2=123456",
+     "http://10.10.1.2:34801/api?key1=abc&key2=123456"},
+    // Test default scheme, specified ip, multi-level path and query with 
multiple keys.
+    {nullptr,
+     "10.10.1.2",
+     34801,
+     "/api/multi/level/path",
+     "key1=abc&key2=123456",
+     "http://10.10.1.2:34801/api/multi/level/path?key1=abc&key2=123456"},
+    // Test specified scheme, ip, path and query with multiple keys.
+    {"http",
+     "10.10.1.2",
+     34801,
+     "/api",
+     "key1=abc&key2=123456",
+     "http://10.10.1.2:34801/api?key1=abc&key2=123456"},
+    // Test specified scheme, ip, multi-level path and query with multiple 
keys.
+    {"http",
+     "10.10.1.2",
+     34801,
+     "/api/multi/level/path",
+     "key1=abc&key2=123456",
+     "http://10.10.1.2:34801/api/multi/level/path?key1=abc&key2=123456"},
+};
+
+INSTANTIATE_TEST_CASE_P(HttpUrlTest, HttpUrlBuildTest, 
testing::ValuesIn(http_url_tests));
+
 TEST(HttpClientTest, Connect)
 {
     http_client client;
@@ -109,12 +367,20 @@ TEST(HttpClientTest, Callback)
     NO_FATALS(check_expected_description_prefix(expected_description_prefix, 
err));
 }
 
-using http_client_method_case =
-    std::tuple<const char *, http_method, const char *, http_status_code, 
const char *>;
+struct http_client_method_case
+{
+    const char *host;
+    uint16_t port;
+    const char *path;
+    http_method method;
+    const char *post_data;
+    http_status_code expected_http_status;
+    const char *expected_response;
+};
 
 class HttpClientMethodTest : public 
testing::TestWithParam<http_client_method_case>
 {
-public:
+protected:
     void SetUp() override { ASSERT_TRUE(_client.init()); }
 
     void test_method_with_response_string(const http_status_code 
expected_http_status,
@@ -153,14 +419,11 @@ public:
         EXPECT_EQ(expected_http_status, actual_http_status);
     }
 
-    void test_mothod(const std::string &url,
-                     const http_method method,
+    void test_mothod(const http_method method,
                      const std::string &post_data,
                      const http_status_code expected_http_status,
                      const std::string &expected_response)
     {
-        ASSERT_TRUE(_client.set_url(url));
-
         switch (method) {
         case http_method::GET:
             ASSERT_TRUE(_client.with_get_method());
@@ -176,40 +439,86 @@ public:
         test_method_with_response_callback(expected_http_status, 
expected_response);
     }
 
-private:
     http_client _client;
 };
 
-TEST_P(HttpClientMethodTest, ExecMethod)
+#define TEST_HTTP_CLIENT_EXEC_METHOD(url_setter)                               
                    \
+    do {                                                                       
                    \
+        const auto &method_case = GetParam();                                  
                    \
+        url_setter(method_case.host, method_case.port, method_case.path);      
                    \
+        test_mothod(method_case.method,                                        
                    \
+                    method_case.post_data,                                     
                    \
+                    method_case.expected_http_status,                          
                    \
+                    method_case.expected_response);                            
                    \
+    } while (0)
+
+// Test setting url by string, where set_url returns an error_s which should 
be checked.
+#define SET_HTTP_CLIENT_BY_URL_STRING(host, port, path)                        
                    \
+    do {                                                                       
                    \
+        const auto &url = fmt::format("http://{}:{}{}";, host, port, path);     
                    \
+        ASSERT_TRUE(_client.set_url(url));                                     
                    \
+    } while (0)
+
+TEST_P(HttpClientMethodTest, ExecMethodByUrlString)
 {
-    const char *url;
-    http_method method;
-    const char *post_data;
-    http_status_code expected_http_status;
-    const char *expected_response;
-    std::tie(url, method, post_data, expected_http_status, expected_response) 
= GetParam();
+    TEST_HTTP_CLIENT_EXEC_METHOD(SET_HTTP_CLIENT_BY_URL_STRING);
+}
 
-    http_client _client;
-    test_mothod(url, method, post_data, expected_http_status, 
expected_response);
+// Test setting url by copying url object, where set_url returns nothing.
+#define SET_HTTP_CLIENT_BY_COPY_URL_OBJECT(host, port, path)                   
                    \
+    do {                                                                       
                    \
+        http_url url;                                                          
                    \
+        ASSERT_TRUE(url.set_host(host));                                       
                    \
+        ASSERT_TRUE(url.set_port(port));                                       
                    \
+        ASSERT_TRUE(url.set_path(path));                                       
                    \
+        _client.set_url(url);                                                  
                    \
+    } while (0)
+
+TEST_P(HttpClientMethodTest, ExecMethodByCopyUrlClass)
+{
+    TEST_HTTP_CLIENT_EXEC_METHOD(SET_HTTP_CLIENT_BY_COPY_URL_OBJECT);
+}
+
+// Test setting url by moving url object, where set_url returns nothing.
+#define SET_HTTP_CLIENT_BY_MOVE_URL_OBJECT(host, port, path)                   
                    \
+    do {                                                                       
                    \
+        http_url url;                                                          
                    \
+        ASSERT_TRUE(url.set_host(host));                                       
                    \
+        ASSERT_TRUE(url.set_port(port));                                       
                    \
+        ASSERT_TRUE(url.set_path(path));                                       
                    \
+        _client.set_url(std::move(url));                                       
                    \
+    } while (0)
+
+TEST_P(HttpClientMethodTest, ExecMethodByMoveUrlClass)
+{
+    TEST_HTTP_CLIENT_EXEC_METHOD(SET_HTTP_CLIENT_BY_MOVE_URL_OBJECT);
 }
 
 const std::vector<http_client_method_case> http_client_method_tests = {
-    {"http://127.0.0.1:20001/test/get";,
+    {"127.0.0.1",
+     20001,
+     "/test/get",
      http_method::POST,
      "with POST DATA",
      http_status_code::kBadRequest,
      "please use GET method"},
-    {"http://127.0.0.1:20001/test/get";,
+    {"127.0.0.1",
+     20001,
+     "/test/get",
      http_method::GET,
      "",
      http_status_code::kOk,
      "you are using GET method"},
-    {"http://127.0.0.1:20001/test/post";,
+    {"127.0.0.1",
+     20001,
+     "/test/post",
      http_method::POST,
      "with POST DATA",
      http_status_code::kOk,
      "you are using POST method with POST DATA"},
-    {"http://127.0.0.1:20001/test/post";,
+    {"127.0.0.1",
+     20001,
+     "/test/post",
      http_method::GET,
      "",
      http_status_code::kBadRequest,
diff --git a/src/utils/enum_helper.h b/src/utils/enum_helper.h
index b408f5e35..b62918c53 100644
--- a/src/utils/enum_helper.h
+++ b/src/utils/enum_helper.h
@@ -179,7 +179,8 @@ class enum_helper_xxx;
     helper->register_enum(#str, enum_class::ENUM_CONST(str));
 
 #define ENUM_CONST_DEF_MAPPER(direction, from_type, to_type, enum_foreach, 
enum_def)               \
-    inline to_type enum_##direction##_val(from_type from_val, to_type 
invalid_to_val)              \
+    inline to_type enum_##direction##_val(const from_type &from_val,           
                    \
+                                          const to_type &invalid_to_val)       
                    \
     {                                                                          
                    \
         static const std::unordered_map<from_type, to_type> kMap = 
{enum_foreach(enum_def)};       \
                                                                                
                    \


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to