Fix URL encoding issues; remove request shortcuts.

master
Chunting Gu 6 years ago
parent 9b31ac855a
commit 1a21989b2e

@ -51,22 +51,6 @@ TEST(ClientTest, Head) {
} }
} }
TEST(ClientTest, Head_Shortcut) {
webcc::ClientSession session;
try {
auto r = session.Head("http://httpbin.org/get");
EXPECT_EQ(webcc::Status::kOK, r->status());
EXPECT_EQ("OK", r->reason());
EXPECT_EQ("", r->data());
} catch (const webcc::Error& error) {
std::cerr << error << std::endl;
}
}
// Force Accept-Encoding to be "identity" so that HttpBin.org will include // Force Accept-Encoding to be "identity" so that HttpBin.org will include
// a Content-Length header in the response. // a Content-Length header in the response.
// This tests that the response with Content-Length while no body could be // This tests that the response with Content-Length while no body could be
@ -136,15 +120,25 @@ TEST(ClientTest, Get) {
} }
} }
TEST(ClientTest, Get_Shortcut) { // Test the space in the query string could be encoded.
TEST(ClientTest, Get_QueryEncode) {
webcc::ClientSession session; webcc::ClientSession session;
try { try {
auto r = session.Get("http://httpbin.org/get", auto r = session.Request(webcc::RequestBuilder{}.
{ "key1", "value1", "key2", "value2" }, Get("http://httpbin.org/get").
{ "Accept", "application/json" }); Query("name", "Chunting Gu", true)
());
AssertGet(r); EXPECT_EQ(webcc::Status::kOK, r->status());
EXPECT_EQ("OK", r->reason());
Json::Value json = StringToJson(r->data());
Json::Value args = json["args"];
EXPECT_EQ(1, args.size());
EXPECT_EQ("Chunting Gu", args["name"].asString());
} catch (const webcc::Error& error) { } catch (const webcc::Error& error) {
std::cerr << error << std::endl; std::cerr << error << std::endl;
@ -323,26 +317,6 @@ TEST(ClientTest, Post) {
} }
} }
TEST(ClientTest, Post_Shortcut) {
webcc::ClientSession session;
try {
const std::string data = "{'name'='Adam', 'age'=20}";
auto r = session.Post("http://httpbin.org/post", std::string(data), true);
EXPECT_EQ(webcc::Status::kOK, r->status());
EXPECT_EQ("OK", r->reason());
Json::Value json = StringToJson(r->data());
EXPECT_EQ(data, json["data"].asString());
} catch (const webcc::Error& error) {
std::cerr << error << std::endl;
}
}
static bfs::path GenerateTempFile(const std::string& data) { static bfs::path GenerateTempFile(const std::string& data) {
try { try {
bfs::path path = bfs::temp_directory_path() / bfs::unique_path(); bfs::path path = bfs::temp_directory_path() / bfs::unique_path();
@ -424,7 +398,10 @@ TEST(ClientTest, Post_Gzip) {
try { try {
// Use Boost.org home page as the POST data. // Use Boost.org home page as the POST data.
auto r1 = session.Get("https://www.boost.org/"); auto r1 = session.Request(webcc::RequestBuilder{}.
Get("https://www.boost.org/")
());
const std::string& data = r1->data(); const std::string& data = r1->data();
auto r2 = session.Request(webcc::RequestBuilder{}. auto r2 = session.Request(webcc::RequestBuilder{}.

@ -14,22 +14,14 @@ int main() {
try { try {
r = session.Request(webcc::RequestBuilder{}. r = session.Request(webcc::RequestBuilder{}.
Get("http://httpbin.org/get"). Get("http://httpbin.org/get").
Query("key1", "value1"). Query("name", "Adam Gu", /*encode*/true).
Query("key2", "value2"). Header("Accept", "application/json").
Date(). Date()
Header("Accept", "application/json")
()); ());
assert(r->status() == webcc::Status::kOK); assert(r->status() == webcc::Status::kOK);
assert(!r->data().empty()); assert(!r->data().empty());
r = session.Get("http://httpbin.org/get",
{ "key1", "value1", "key2", "value2" },
{ "Accept", "application/json" });
assert(r->status() == webcc::Status::kOK);
assert(!r->data().empty());
r = session.Request(webcc::RequestBuilder{}. r = session.Request(webcc::RequestBuilder{}.
Post("http://httpbin.org/post"). Post("http://httpbin.org/post").
Body("{'name'='Adam', 'age'=20}"). Body("{'name'='Adam', 'age'=20}").
@ -41,7 +33,9 @@ int main() {
#if WEBCC_ENABLE_SSL #if WEBCC_ENABLE_SSL
r = session.Get("https://httpbin.org/get"); r = session.Request(webcc::RequestBuilder{}.
Get("https://httpbin.org/get")
());
assert(r->status() == webcc::Status::kOK); assert(r->status() == webcc::Status::kOK);
assert(!r->data().empty()); assert(!r->data().empty());
@ -51,9 +45,6 @@ int main() {
} catch (const webcc::Error& error) { } catch (const webcc::Error& error) {
std::cerr << error << std::endl; std::cerr << error << std::endl;
return 1; return 1;
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return 1;
} }
return 0; return 0;

@ -55,7 +55,9 @@ void PrettyPrintJsonString(const std::string& str) {
// List public events. // List public events.
void ListEvents(webcc::ClientSession& session) { void ListEvents(webcc::ClientSession& session) {
try { try {
auto r = session.Get(kUrlRoot + "/events"); auto r = session.Request(webcc::RequestBuilder{}.
Get(kUrlRoot).Path("events")
());
PRINT_JSON_STRING(r->data()); PRINT_JSON_STRING(r->data());
@ -66,10 +68,13 @@ void ListEvents(webcc::ClientSession& session) {
// List the followers of the given user. // List the followers of the given user.
// Example: // Example:
// ListUserFollowers(session, "<login>") // ListUserFollowers(session, "<user>")
void ListUserFollowers(webcc::ClientSession& session, const std::string& user) { void ListUserFollowers(webcc::ClientSession& session, const std::string& user) {
try { try {
auto r = session.Get(kUrlRoot + "/users/" + user + "/followers"); auto r = session.Request(webcc::RequestBuilder{}.
Get(kUrlRoot).Path("users").Path(user).
Path("followers")
());
PRINT_JSON_STRING(r->data()); PRINT_JSON_STRING(r->data());
@ -86,7 +91,7 @@ void ListAuthUserFollowers(webcc::ClientSession& session,
const std::string& password) { const std::string& password) {
try { try {
auto r = session.Request(webcc::RequestBuilder{}. auto r = session.Request(webcc::RequestBuilder{}.
Get(kUrlRoot + "/user/followers"). Get(kUrlRoot).Path("user/followers").
AuthBasic(login, password) AuthBasic(login, password)
()); ());
@ -108,7 +113,7 @@ void CreateAuthorization(webcc::ClientSession& session,
"}"; "}";
auto r = session.Request(webcc::RequestBuilder{}. auto r = session.Request(webcc::RequestBuilder{}.
Post(kUrlRoot + "/authorizations"). Post(kUrlRoot).Path("authorizations").
Body(std::move(data)). Body(std::move(data)).
Json().Utf8(). Json().Utf8().
AuthBasic(login, password) AuthBasic(login, password)
@ -130,5 +135,8 @@ int main() {
ListEvents(session); ListEvents(session);
//ListUserFollowers(session, "sprinfall");
//ListAuthUserFollowers(session, "sprinfall@gmail.com", "<password>");
return 0; return 0;
} }

@ -57,7 +57,7 @@ BookClient::BookClient(const std::string& url, int timeout)
bool BookClient::ListBooks(std::list<Book>* books) { bool BookClient::ListBooks(std::list<Book>* books) {
try { try {
auto r = session_.Get(url_ + "/books"); auto r = session_.Request(WEBCC_GET(url_).Path("books")());
if (!CheckStatus(r, webcc::Status::kOK)) { if (!CheckStatus(r, webcc::Status::kOK)) {
// Response HTTP status error. // Response HTTP status error.
@ -89,7 +89,9 @@ bool BookClient::CreateBook(const std::string& title, double price,
req_json["price"] = price; req_json["price"] = price;
try { try {
auto r = session_.Post(url_ + "/books", JsonToString(req_json), true); auto r = session_.Request(WEBCC_POST(url_).Path("books").
Body(JsonToString(req_json))
());
if (!CheckStatus(r, webcc::Status::kCreated)) { if (!CheckStatus(r, webcc::Status::kCreated)) {
return false; return false;
@ -108,7 +110,7 @@ bool BookClient::CreateBook(const std::string& title, double price,
bool BookClient::GetBook(const std::string& id, Book* book) { bool BookClient::GetBook(const std::string& id, Book* book) {
try { try {
auto r = session_.Get(url_ + "/books/" + id); auto r = session_.Request(WEBCC_GET(url_).Path("books").Path(id)());
if (!CheckStatus(r, webcc::Status::kOK)) { if (!CheckStatus(r, webcc::Status::kOK)) {
return false; return false;
@ -129,7 +131,9 @@ bool BookClient::UpdateBook(const std::string& id, const std::string& title,
json["price"] = price; json["price"] = price;
try { try {
auto r = session_.Put(url_ + "/books/" + id, JsonToString(json), true); auto r = session_.Request(WEBCC_PUT(url_).Path("books").Path(id).
Body(JsonToString(json))
());
if (!CheckStatus(r, webcc::Status::kOK)) { if (!CheckStatus(r, webcc::Status::kOK)) {
return false; return false;
@ -145,7 +149,7 @@ bool BookClient::UpdateBook(const std::string& id, const std::string& title,
bool BookClient::DeleteBook(const std::string& id) { bool BookClient::DeleteBook(const std::string& id) {
try { try {
auto r = session_.Delete(url_ + "/books/" + id); auto r = session_.Request(WEBCC_DELETE(url_).Path("books").Path(id)());
if (!CheckStatus(r, webcc::Status::kOK)) { if (!CheckStatus(r, webcc::Status::kOK)) {
return false; return false;

@ -3,7 +3,7 @@
#include "webcc/url.h" #include "webcc/url.h"
TEST(UrlTest, Basic) { TEST(UrlTest, Basic) {
webcc::Url url("http://example.com/path", false); webcc::Url url("http://example.com/path");
EXPECT_EQ("http", url.scheme()); EXPECT_EQ("http", url.scheme());
EXPECT_EQ("example.com", url.host()); EXPECT_EQ("example.com", url.host());
@ -13,7 +13,7 @@ TEST(UrlTest, Basic) {
} }
TEST(UrlTest, NoPath) { TEST(UrlTest, NoPath) {
webcc::Url url("http://example.com", false); webcc::Url url("http://example.com");
EXPECT_EQ("http", url.scheme()); EXPECT_EQ("http", url.scheme());
EXPECT_EQ("example.com", url.host()); EXPECT_EQ("example.com", url.host());
@ -23,7 +23,7 @@ TEST(UrlTest, NoPath) {
} }
TEST(UrlTest, NoPath2) { TEST(UrlTest, NoPath2) {
webcc::Url url("http://example.com/", false); webcc::Url url("http://example.com/");
EXPECT_EQ("http", url.scheme()); EXPECT_EQ("http", url.scheme());
EXPECT_EQ("example.com", url.host()); EXPECT_EQ("example.com", url.host());
@ -33,7 +33,7 @@ TEST(UrlTest, NoPath2) {
} }
TEST(UrlTest, NoPath3) { TEST(UrlTest, NoPath3) {
webcc::Url url("http://example.com?key=value", false); webcc::Url url("http://example.com?key=value");
EXPECT_EQ("http", url.scheme()); EXPECT_EQ("http", url.scheme());
EXPECT_EQ("example.com", url.host()); EXPECT_EQ("example.com", url.host());
@ -43,7 +43,7 @@ TEST(UrlTest, NoPath3) {
} }
TEST(UrlTest, NoPath4) { TEST(UrlTest, NoPath4) {
webcc::Url url("http://example.com/?key=value", false); webcc::Url url("http://example.com/?key=value");
EXPECT_EQ("http", url.scheme()); EXPECT_EQ("http", url.scheme());
EXPECT_EQ("example.com", url.host()); EXPECT_EQ("example.com", url.host());
@ -53,7 +53,7 @@ TEST(UrlTest, NoPath4) {
} }
TEST(UrlTest, NoScheme) { TEST(UrlTest, NoScheme) {
webcc::Url url("/path/to", false); webcc::Url url("/path/to");
EXPECT_EQ("", url.scheme()); EXPECT_EQ("", url.scheme());
EXPECT_EQ("", url.host()); EXPECT_EQ("", url.host());
@ -63,7 +63,7 @@ TEST(UrlTest, NoScheme) {
} }
TEST(UrlTest, NoScheme2) { TEST(UrlTest, NoScheme2) {
webcc::Url url("/path/to?key=value", false); webcc::Url url("/path/to?key=value");
EXPECT_EQ("", url.scheme()); EXPECT_EQ("", url.scheme());
EXPECT_EQ("", url.host()); EXPECT_EQ("", url.host());
@ -73,7 +73,7 @@ TEST(UrlTest, NoScheme2) {
} }
TEST(UrlTest, Full) { TEST(UrlTest, Full) {
webcc::Url url("https://localhost:3000/path/to?key=value", false); webcc::Url url("https://localhost:3000/path/to?key=value");
EXPECT_EQ("https", url.scheme()); EXPECT_EQ("https", url.scheme());
EXPECT_EQ("localhost", url.host()); EXPECT_EQ("localhost", url.host());

@ -3,14 +3,92 @@
#include "webcc/utility.h" #include "webcc/utility.h"
TEST(UtilityTest, SplitKV) { TEST(UtilityTest, SplitKV) {
const std::string str = "Connection: Keep-Alive"; const std::string str = "key=value";
std::string key; std::string key;
std::string value; std::string value;
bool ok = webcc::utility::SplitKV(str, ':', &key, &value); bool ok = webcc::utility::SplitKV(str, '=', &key, &value);
EXPECT_EQ(true, ok); EXPECT_EQ(true, ok);
EXPECT_EQ("Connection", key); EXPECT_EQ("key", key);
EXPECT_EQ("Keep-Alive", value); EXPECT_EQ("value", value);
}
TEST(UtilityTest, SplitKV_OtherDelim) {
const std::string str = "key:value";
std::string key;
std::string value;
bool ok = webcc::utility::SplitKV(str, ':', &key, &value);
EXPECT_TRUE(ok);
EXPECT_EQ("key", key);
EXPECT_EQ("value", value);
}
TEST(UtilityTest, SplitKV_Spaces) {
const std::string str = " key = value ";
std::string key;
std::string value;
bool ok = webcc::utility::SplitKV(str, '=', &key, &value);
EXPECT_TRUE(ok);
EXPECT_EQ("key", key);
EXPECT_EQ("value", value);
}
TEST(UtilityTest, SplitKV_SpacesNoTrim) {
const std::string str = " key = value ";
std::string key;
std::string value;
bool ok = webcc::utility::SplitKV(str, '=', &key, &value, false);
EXPECT_TRUE(ok);
EXPECT_EQ(" key ", key);
EXPECT_EQ(" value ", value);
}
TEST(UtilityTest, SplitKV_NoKey) {
const std::string str = "=value";
std::string key;
std::string value;
bool ok = webcc::utility::SplitKV(str, '=', &key, &value);
EXPECT_TRUE(ok);
EXPECT_EQ("", key);
EXPECT_EQ("value", value);
}
TEST(UtilityTest, SplitKV_NoValue) {
const std::string str = "key=";
std::string key;
std::string value;
bool ok = webcc::utility::SplitKV(str, '=', &key, &value);
EXPECT_TRUE(ok);
EXPECT_EQ("key", key);
EXPECT_EQ("", value);
}
TEST(UtilityTest, SplitKV_NoKeyNoValue) {
const std::string str = "=";
std::string key;
std::string value;
bool ok = webcc::utility::SplitKV(str, '=', &key, &value);
EXPECT_TRUE(ok);
EXPECT_EQ("", key);
EXPECT_EQ("", value);
} }

@ -47,104 +47,6 @@ ResponsePtr ClientSession::Request(RequestPtr request, bool stream) {
return Send(request, stream); return Send(request, stream);
} }
static void SetHeaders(const Strings& headers, RequestBuilder* builder) {
assert(headers.size() % 2 == 0);
for (std::size_t i = 1; i < headers.size(); i += 2) {
builder->Header(headers[i - 1], headers[i]);
}
}
ResponsePtr ClientSession::Get(const std::string& url,
const Strings& parameters,
const Strings& headers) {
RequestBuilder builder;
builder.Get(url);
assert(parameters.size() % 2 == 0);
for (std::size_t i = 1; i < parameters.size(); i += 2) {
builder.Query(parameters[i - 1], parameters[i]);
}
SetHeaders(headers, &builder);
return Request(builder());
}
ResponsePtr ClientSession::Head(const std::string& url,
const Strings& parameters,
const Strings& headers) {
RequestBuilder builder;
builder.Head(url);
assert(parameters.size() % 2 == 0);
for (std::size_t i = 1; i < parameters.size(); i += 2) {
builder.Query(parameters[i - 1], parameters[i]);
}
SetHeaders(headers, &builder);
return Request(builder());
}
ResponsePtr ClientSession::Post(const std::string& url, std::string&& data,
bool json, const Strings& headers) {
RequestBuilder builder;
builder.Post(url);
SetHeaders(headers, &builder);
builder.Body(std::move(data));
if (json) {
builder.Json();
}
return Request(builder());
}
ResponsePtr ClientSession::Put(const std::string& url, std::string&& data,
bool json, const Strings& headers) {
RequestBuilder builder;
builder.Put(url);
SetHeaders(headers, &builder);
builder.Body(std::move(data));
if (json) {
builder.Json();
}
return Request(builder());
}
ResponsePtr ClientSession::Delete(const std::string& url,
const Strings& headers) {
RequestBuilder builder;
builder.Delete(url);
SetHeaders(headers, &builder);
return Request(builder());
}
ResponsePtr ClientSession::Patch(const std::string& url, std::string&& data,
bool json, const Strings& headers) {
RequestBuilder builder;
builder.Patch(url);
SetHeaders(headers, &builder);
builder.Body(std::move(data));
if (json) {
builder.Json();
}
return Request(builder());
}
void ClientSession::InitHeaders() { void ClientSession::InitHeaders() {
using namespace headers; using namespace headers;

@ -59,33 +59,10 @@ public:
// Please use RequestBuilder to build the request. // Please use RequestBuilder to build the request.
// If |stream| is true, the response data will be written into a temp file, // If |stream| is true, the response data will be written into a temp file,
// the response body will be FileBody, and you can easily move the temp file // the response body will be FileBody, and you can easily move the temp file
// to another path with FileBody::Move(). So |stream| is useful for // to another path with FileBody::Move(). So, |stream| is really useful for
// downloading files (JPEG, etc.) or saving memory for huge data responses. // downloading files (JPEG, etc.) or saving memory for huge data responses.
ResponsePtr Request(RequestPtr request, bool stream = false); ResponsePtr Request(RequestPtr request, bool stream = false);
// Shortcut for GET request.
ResponsePtr Get(const std::string& url, const Strings& parameters = {},
const Strings& headers = {});
// Shortcut for HEAD request.
ResponsePtr Head(const std::string& url, const Strings& parameters = {},
const Strings& headers = {});
// Shortcut for POST request.
ResponsePtr Post(const std::string& url, std::string&& data, bool json,
const Strings& headers = {});
// Shortcut for PUT request.
ResponsePtr Put(const std::string& url, std::string&& data, bool json,
const Strings& headers = {});
// Shortcut for DELETE request.
ResponsePtr Delete(const std::string& url, const Strings& headers = {});
// Shortcut for PATCH request.
ResponsePtr Patch(const std::string& url, std::string&& data, bool json,
const Strings& headers = {});
private: private:
void InitHeaders(); void InitHeaders();

@ -31,10 +31,7 @@ void Request::Prepare() {
target += url_.query(); target += url_.query();
} }
start_line_ = method_; start_line_ = method_ + " " + target + " HTTP/1.1";
start_line_ += " ";
start_line_ += target;
start_line_ += " HTTP/1.1";
if (url_.port().empty()) { if (url_.port().empty()) {
SetHeader(headers::kHost, url_.host()); SetHeader(headers::kHost, url_.host());

@ -51,7 +51,25 @@ RequestPtr RequestBuilder::operator()() {
return request; return request;
} }
RequestBuilder& RequestBuilder::File(const Path& path, bool infer_media_type, RequestBuilder& RequestBuilder::Url(const std::string& url, bool encode) {
url_ = webcc::Url{ url, encode };
return *this;
}
RequestBuilder& RequestBuilder::Path(const std::string& path, bool encode) {
url_.AppendPath(path, encode);
return *this;
}
RequestBuilder& RequestBuilder::Query(const std::string& key,
const std::string& value,
bool encode) {
url_.AppendQuery(key, value, encode);
return *this;
}
RequestBuilder& RequestBuilder::File(const webcc::Path& path,
bool infer_media_type,
std::size_t chunk_size) { std::size_t chunk_size) {
body_.reset(new FileBody{ path, chunk_size }); body_.reset(new FileBody{ path, chunk_size });
@ -63,7 +81,7 @@ RequestBuilder& RequestBuilder::File(const Path& path, bool infer_media_type,
} }
RequestBuilder& RequestBuilder::FormFile(const std::string& name, RequestBuilder& RequestBuilder::FormFile(const std::string& name,
const Path& path, const webcc::Path& path,
const std::string& media_type) { const std::string& media_type) {
assert(!name.empty()); assert(!name.empty());
return Form(FormPart::NewFile(name, path, media_type)); return Form(FormPart::NewFile(name, path, media_type));

@ -7,6 +7,24 @@
#include "webcc/request.h" #include "webcc/request.h"
#include "webcc/url.h" #include "webcc/url.h"
// -----------------------------------------------------------------------------
// Handy macros for creating a RequestBuilder.
#define WEBCC_GET(url) webcc::RequestBuilder{}.Get(url, false)
#define WEBCC_GET_ENC(url) webcc::RequestBuilder{}.Get(url, true)
#define WEBCC_HEAD(url) webcc::RequestBuilder{}.Head(url, false)
#define WEBCC_HEAD_ENC(url) webcc::RequestBuilder{}.Head(url, true)
#define WEBCC_POST(url) webcc::RequestBuilder{}.Post(url, false)
#define WEBCC_POST_ENC(url) webcc::RequestBuilder{}.Post(url, true)
#define WEBCC_PUT(url) webcc::RequestBuilder{}.Put(url, false)
#define WEBCC_PUT_ENC(url) webcc::RequestBuilder{}.Put(url, true)
#define WEBCC_DELETE(url) webcc::RequestBuilder{}.Delete(url, false)
#define WEBCC_DELETE_ENC(url) webcc::RequestBuilder{}.Delete(url, true)
#define WEBCC_PATCH(url) webcc::RequestBuilder{}.Patch(url, false)
#define WEBCC_PATCH_ENC(url) webcc::RequestBuilder{}.Patch(url, true)
// -----------------------------------------------------------------------------
namespace webcc { namespace webcc {
class RequestBuilder { class RequestBuilder {
@ -16,52 +34,46 @@ public:
RequestBuilder(const RequestBuilder&) = delete; RequestBuilder(const RequestBuilder&) = delete;
RequestBuilder& operator=(const RequestBuilder&) = delete; RequestBuilder& operator=(const RequestBuilder&) = delete;
// Build the request. // Build
RequestPtr operator()(); RequestPtr operator()();
// NOTE:
// The naming convention doesn't follow Google C++ Style for
// consistency and simplicity.
RequestBuilder& Method(const std::string& method) { RequestBuilder& Method(const std::string& method) {
method_ = method; method_ = method;
return *this; return *this;
} }
RequestBuilder& Url(const std::string& url) { RequestBuilder& Get(const std::string& url, bool encode = false) {
url_.Init(url); return Method(methods::kGet).Url(url, encode);
return *this;
} }
RequestBuilder& Get(const std::string& url) { RequestBuilder& Head(const std::string& url, bool encode = false) {
return Method(methods::kGet).Url(url); return Method(methods::kHead).Url(url, encode);
} }
RequestBuilder& Head(const std::string& url) { RequestBuilder& Post(const std::string& url, bool encode = false) {
return Method(methods::kHead).Url(url); return Method(methods::kPost).Url(url, encode);
} }
RequestBuilder& Post(const std::string& url) { RequestBuilder& Put(const std::string& url, bool encode = false) {
return Method(methods::kPost).Url(url); return Method(methods::kPut).Url(url, encode);
} }
RequestBuilder& Put(const std::string& url) { RequestBuilder& Delete(const std::string& url, bool encode = false) {
return Method(methods::kPut).Url(url); return Method(methods::kDelete).Url(url, encode);
} }
RequestBuilder& Delete(const std::string& url) { RequestBuilder& Patch(const std::string& url, bool encode = false) {
return Method(methods::kDelete).Url(url); return Method(methods::kPatch).Url(url, encode);
} }
RequestBuilder& Patch(const std::string& url) { RequestBuilder& Url(const std::string& url, bool encode = false);
return Method(methods::kPatch).Url(url);
}
// Add a query parameter. // Append a piece to the path.
RequestBuilder& Query(const std::string& key, const std::string& value) { RequestBuilder& Path(const std::string& path, bool encode = false);
url_.AddQuery(key, value);
return *this; // Append a parameter to the query.
} RequestBuilder& Query(const std::string& key, const std::string& value,
bool encode = false);
RequestBuilder& MediaType(const std::string& media_type) { RequestBuilder& MediaType(const std::string& media_type) {
media_type_ = media_type; media_type_ = media_type;
@ -97,7 +109,7 @@ public:
// Use the file content as body. // Use the file content as body.
// NOTE: Error::kFileError might be thrown. // NOTE: Error::kFileError might be thrown.
RequestBuilder& File(const Path& path, bool infer_media_type = true, RequestBuilder& File(const webcc::Path& path, bool infer_media_type = true,
std::size_t chunk_size = 1024); std::size_t chunk_size = 1024);
// Add a form part. // Add a form part.
@ -107,7 +119,7 @@ public:
} }
// Add a form part of file. // Add a form part of file.
RequestBuilder& FormFile(const std::string& name, const Path& path, RequestBuilder& FormFile(const std::string& name, const webcc::Path& path,
const std::string& media_type = ""); const std::string& media_type = "");
// Add a form part of string data. // Add a form part of string data.

@ -22,13 +22,9 @@ public:
ResponseBuilder(const ResponseBuilder&) = delete; ResponseBuilder(const ResponseBuilder&) = delete;
ResponseBuilder& operator=(const ResponseBuilder&) = delete; ResponseBuilder& operator=(const ResponseBuilder&) = delete;
// Build the response. // Build
ResponsePtr operator()(); ResponsePtr operator()();
// NOTE:
// The naming convention doesn't follow Google C++ Style for
// consistency and simplicity.
// Some shortcuts for different status codes: // Some shortcuts for different status codes:
ResponseBuilder& OK() { ResponseBuilder& OK() {

@ -1,15 +1,16 @@
#include "webcc/url.h" #include "webcc/url.h"
#include <algorithm> #include <algorithm>
#include <cctype>
#include <functional> #include <functional>
#include <sstream>
#include "boost/algorithm/string.hpp" #include "boost/algorithm/string/trim.hpp"
#include "webcc/utility.h"
namespace webcc { namespace webcc {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Helper functions to decode URL string.
namespace { namespace {
@ -63,58 +64,59 @@ bool Decode(const std::string& encoded, std::string* raw) {
return true; return true;
} }
// Encodes all characters not in given set determined by given function. // Unsafe decode.
std::string EncodeImpl(const std::string& raw, // Return the original string on failure.
std::string DecodeUnsafe(const std::string& encoded) {
std::string raw;
if (Decode(encoded, &raw)) {
return raw;
}
return encoded;
}
// Encode all characters which should be encoded.
std::string EncodeImpl(const std::string& raw, // UTF8
std::function<bool(int)> should_encode) { std::function<bool(int)> should_encode) {
const char* const hex = "0123456789ABCDEF"; const char kHex[] = "0123456789ABCDEF";
std::string encoded; std::string encoded;
for (auto iter = raw.begin(); iter != raw.end(); ++iter) { for (auto i = raw.begin(); i != raw.end(); ++i) {
// For UTF8 encoded string, char ASCII can be greater than 127. // For UTF8 encoded string, char ASCII can be greater than 127.
int ch = static_cast<unsigned char>(*iter); int c = static_cast<int>(*i);
// |ch| should be the same under both UTF8 and UTF16. if (should_encode(c)) {
if (should_encode(ch)) {
encoded.push_back('%'); encoded.push_back('%');
encoded.push_back(hex[(ch >> 4) & 0xF]); encoded.push_back(kHex[(c >> 4) & 0xF]);
encoded.push_back(hex[ch & 0xF]); encoded.push_back(kHex[c & 0xF]);
} else { } else {
// ASCII doesn't need to be encoded, it should be the same under both encoded.push_back(static_cast<char>(c));
// UTF8 and UTF16.
encoded.push_back(static_cast<char>(ch));
} }
} }
return encoded; return encoded;
} }
// Our own implementation of alpha numeric instead of std::isalnum to avoid // Characters that are allowed in a URI but do not have a reserved purpose are
// taking global lock for performance reasons. // are called unreserved. These include uppercase and lowercase letters, decimal
inline bool IsAlphaNumeric(char c) { // digits, hyphen, period, underscore, and tilde.
return (c >= '0' && c <= '9') || inline bool IsUnreserved(int c) {
(c >= 'A' && c <= 'Z') || return std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~';
(c >= 'a' && c <= 'z');
} }
// Unreserved characters are those that are allowed in a URL/URI but do not have // General delimiters serve as the delimiters between different uri components.
// a reserved purpose. They include: // General delimiters include:
// - A-Z // - All of these :/?#[]@
// - a-z inline bool IsGenDelim(int c) {
// - 0-9 return c == ':' || c == '/' || c == '?' || c == '#' || c == '[' || c == ']' ||
// - '-' (hyphen) c == '@';
// - '.' (period)
// - '_' (underscore)
// - '~' (tilde)
inline bool IsUnreserved(int c) {
return IsAlphaNumeric((char)c) ||
c == '-' || c == '.' || c == '_' || c == '~';
} }
// Sub-delimiters are those characters that may have a defined meaning within // Sub-delimiters are those characters that may have a defined meaning within
// component of a URL/URI for a particular scheme. They do not serve as // component of a URL/URI for a particular scheme. They do not serve as
// delimiters in any case between URL/URI segments. Sub-delimiters include: // delimiters in any case between URL/URI segments. Sub-delimiters include:
// - All of these !$&'()*+,;= // - All of these !$&'()*+,;=
inline bool SubDelimiter(int c) { bool IsSubDelim(int c) {
switch (c) { switch (c) {
case '!': case '!':
case '$': case '$':
@ -133,8 +135,20 @@ inline bool SubDelimiter(int c) {
} }
} }
// Reserved characters includes the general delimiters and sub delimiters. Some
// characters are neither reserved nor unreserved, and must be percent-encoded.
inline bool IsReserved(int c) {
return IsGenDelim(c) || IsSubDelim(c);
}
// Legal characters in the path portion include:
// - Any unreserved character
// - The percent character ('%'), and thus any percent-encoded octet
// - The sub-delimiters
// - ':' (colon)
// - '@' (at sign)
inline bool IsPathChar(int c) { inline bool IsPathChar(int c) {
return IsUnreserved(c) || SubDelimiter(c) || return IsUnreserved(c) || IsSubDelim(c) ||
c == '%' || c == '/' || c == ':' || c == '@'; c == '%' || c == '/' || c == ':' || c == '@';
} }
@ -145,57 +159,75 @@ inline bool IsQueryChar(int c) {
return IsPathChar(c) || c == '?'; return IsPathChar(c) || c == '?';
} }
// Encode the URL query string. } // namespace
inline std::string EncodeQuery(const std::string& query) {
return EncodeImpl(query, [](int c) { // -----------------------------------------------------------------------------
return !IsQueryChar(c) || c == '%' || c == '+';
}); std::string Url::EncodeHost(const std::string& utf8_str) {
return EncodeImpl(utf8_str, [](int c) -> bool { return c > 127; });
} }
bool SplitKeyValue(const std::string& kv, std::string* key, std::string Url::EncodePath(const std::string& utf8_str) {
std::string* value) { return EncodeImpl(utf8_str, [](int c) -> bool {
std::size_t i = kv.find_first_of('='); return !IsPathChar(c) || c == '%' || c == '+';
if (i == std::string::npos || i == 0) { });
return false; }
}
*key = kv.substr(0, i); std::string Url::EncodeQuery(const std::string& utf8_str) {
*value = kv.substr(i + 1); return EncodeImpl(utf8_str, [](int c) -> bool {
return true; return !IsQueryChar(c) || c == '%' || c == '+';
});
} }
} // namespace std::string Url::EncodeFull(const std::string& utf8_str) {
return EncodeImpl(utf8_str, [](int c) -> bool {
return !IsUnreserved(c) && !IsReserved(c);
});
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
Url::Url(const std::string& str, bool decode) { Url::Url(const std::string& str, bool encode) {
Init(str, decode); if (encode) {
Parse(Url::EncodeFull(str));
} else {
Parse(str);
}
} }
void Url::Init(const std::string& str, bool decode, bool clear) { void Url::AppendPath(const std::string& piece, bool encode) {
if (clear) { if (piece.empty() || piece == "/") {
Clear(); return;
} }
if (!decode || str.find('%') == std::string::npos) { if (path_.empty() || path_ == "/") {
Parse(str); path_.clear();
return; if (piece.front() != '/') {
path_.push_back('/');
}
} else if (path_.back() == '/' && piece.front() == '/') {
path_.pop_back();
} else if (path_.back() != '/' && piece.front() != '/') {
path_.push_back('/');
} }
std::string decoded; if (encode) {
if (Decode(str, &decoded)) { path_.append(Url::EncodePath(piece));
Parse(decoded);
} else { } else {
// TODO: Exception? path_.append(piece);
Parse(str);
} }
} }
void Url::AddQuery(const std::string& key, const std::string& value) { void Url::AppendQuery(const std::string& key, const std::string& value,
bool encode) {
if (!query_.empty()) { if (!query_.empty()) {
query_ += "&"; query_ += "&";
} }
query_ += key + "=" + value; if (encode) {
query_ += Url::EncodeQuery(key) + "=" + Url::EncodeQuery(value);
} else {
query_ += key + "=" + value;
}
} }
void Url::Parse(const std::string& str) { void Url::Parse(const std::string& str) {
@ -253,34 +285,43 @@ void Url::Clear() {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
UrlQuery::UrlQuery(const std::string& str) { UrlQuery::UrlQuery(const std::string& encoded_str) {
if (!str.empty()) { if (!encoded_str.empty()) {
// Split into key value pairs separated by '&'. // Split into key value pairs separated by '&'.
for (std::size_t i = 0; i != std::string::npos;) { for (std::size_t i = 0; i != std::string::npos;) {
std::size_t j = str.find_first_of('&', i); std::size_t j = encoded_str.find_first_of('&', i);
std::string kv; std::string kv;
if (j == std::string::npos) { if (j == std::string::npos) {
kv = str.substr(i); kv = encoded_str.substr(i);
i = std::string::npos; i = std::string::npos;
} else { } else {
kv = str.substr(i, j - i); kv = encoded_str.substr(i, j - i);
i = j + 1; i = j + 1;
} }
std::string key; std::string key;
std::string value; std::string value;
if (SplitKeyValue(kv, &key, &value)) { if (utility::SplitKV(kv, '=', &key, &value, false)) {
Add(std::move(key), std::move(value)); parameters_.push_back({ DecodeUnsafe(key), DecodeUnsafe(value) });
} }
} }
} }
} }
void UrlQuery::Add(std::string&& key, std::string&& value) { const std::string& UrlQuery::Get(const std::string& key) const {
if (!Has(key)) { auto it = Find(key);
parameters_.push_back({ std::move(key), std::move(value) }); if (it != parameters_.end()) {
return it->second;
} }
static const std::string kEmptyValue;
return kEmptyValue;
}
const UrlQuery::Parameter& UrlQuery::Get(std::size_t index) const {
assert(index < Size());
return parameters_[index];
} }
void UrlQuery::Add(const std::string& key, const std::string& value) { void UrlQuery::Add(const std::string& key, const std::string& value) {
@ -296,30 +337,21 @@ void UrlQuery::Remove(const std::string& key) {
} }
} }
const std::string& UrlQuery::Get(const std::string& key) const {
auto it = Find(key);
if (it != parameters_.end()) {
return it->second;
}
static const std::string kEmptyValue;
return kEmptyValue;
}
std::string UrlQuery::ToString() const { std::string UrlQuery::ToString() const {
if (parameters_.empty()) { if (parameters_.empty()) {
return ""; return "";
} }
std::string str = parameters_[0].first + "=" + parameters_[0].second; std::string str;
for (std::size_t i = 1; i < parameters_.size(); ++i) { for (std::size_t i = 0; i < parameters_.size(); ++i) {
str += "&"; if (i != 0) {
str += "&";
}
str += parameters_[i].first + "=" + parameters_[i].second; str += parameters_[i].first + "=" + parameters_[i].second;
} }
str = EncodeQuery(str); return Url::EncodeQuery(str);
return str;
} }
UrlQuery::ConstIterator UrlQuery::Find(const std::string& key) const { UrlQuery::ConstIterator UrlQuery::Find(const std::string& key) const {

@ -12,12 +12,20 @@ namespace webcc {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// A simplified implementation of URL (or URI). // A simple implementation of URL (or URI).
// TODO: Encoding of path
class Url { class Url {
public:
// Encode URL different components.
static std::string EncodeHost(const std::string& utf8_str);
static std::string EncodePath(const std::string& utf8_str);
static std::string EncodeQuery(const std::string& utf8_str);
static std::string EncodeFull(const std::string& utf8_str);
public: public:
Url() = default; Url() = default;
explicit Url(const std::string& str, bool decode = true); explicit Url(const std::string& str, bool encode = false);
#if WEBCC_DEFAULT_MOVE_COPY_ASSIGN #if WEBCC_DEFAULT_MOVE_COPY_ASSIGN
@ -47,8 +55,6 @@ public:
#endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN #endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN
void Init(const std::string& str, bool decode = true, bool clear = true);
const std::string& scheme() const { const std::string& scheme() const {
return scheme_; return scheme_;
} }
@ -69,15 +75,19 @@ public:
return query_; return query_;
} }
// Add a query parameter. // Append a piece of path.
void AddQuery(const std::string& key, const std::string& value); void AppendPath(const std::string& piece, bool encode = false);
// Append a query parameter.
void AppendQuery(const std::string& key, const std::string& value,
bool encode = false);
private: private:
void Parse(const std::string& str); void Parse(const std::string& str);
void Clear(); void Clear();
// TODO: Support auth & fragment. private:
std::string scheme_; std::string scheme_;
std::string host_; std::string host_;
std::string port_; std::string port_;
@ -87,35 +97,38 @@ private:
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// URL query parameters. // For accessing URL query parameters.
class UrlQuery { class UrlQuery {
public: public:
using Parameter = std::pair<std::string, std::string>; using Parameter = std::pair<std::string, std::string>;
UrlQuery() = default; // The query string should be key-value pairs separated by '&'.
explicit UrlQuery(const std::string& encoded_str);
// The query string should be key value pairs separated by '&'.
explicit UrlQuery(const std::string& str);
void Add(const std::string& key, const std::string& value); bool Empty() const {
return parameters_.empty();
}
void Add(std::string&& key, std::string&& value); std::size_t Size() const {
return parameters_.size();
}
void Remove(const std::string& key); bool Has(const std::string& key) const {
return Find(key) != parameters_.end();
}
// Get a value by key. // Get a value by key.
// Return empty string if the key doesn't exist. // Return empty string if the key doesn't exist.
const std::string& Get(const std::string& key) const; const std::string& Get(const std::string& key) const;
bool Has(const std::string& key) const { // Get a key-value pair by index.
return Find(key) != parameters_.end(); const Parameter& Get(std::size_t index) const;
}
bool IsEmpty() const { void Add(const std::string& key, const std::string& value);
return parameters_.empty();
} void Remove(const std::string& key);
// Return key-value pairs concatenated by '&'. // Return encoded query string joined with '&'.
// E.g., "item=12731&color=blue&size=large". // E.g., "item=12731&color=blue&size=large".
std::string ToString() const; std::string ToString() const;

@ -35,18 +35,20 @@ std::string GetTimestamp() {
return ss.str(); return ss.str();
} }
bool SplitKV(const std::string& str, char delimiter, bool SplitKV(const std::string& str, char delimiter, std::string* key,
std::string* part1, std::string* part2) { std::string* value, bool trim) {
std::size_t pos = str.find(delimiter); std::size_t pos = str.find(delimiter);
if (pos == std::string::npos) { if (pos == std::string::npos) {
return false; return false;
} }
*part1 = str.substr(0, pos); *key = str.substr(0, pos);
*part2 = str.substr(pos + 1); *value = str.substr(pos + 1);
boost::trim(*part1); if (trim) {
boost::trim(*part2); boost::trim(*key);
boost::trim(*value);
}
return true; return true;
} }

@ -21,8 +21,8 @@ std::string GetTimestamp();
// Split a key-value string. // Split a key-value string.
// E.g., split "Connection: Keep-Alive". // E.g., split "Connection: Keep-Alive".
bool SplitKV(const std::string& str, char delimiter, bool SplitKV(const std::string& str, char delimiter, std::string* key,
std::string* key, std::string* value); std::string* value, bool trim = true);
// Convert string to size_t. // Convert string to size_t.
bool ToSize(const std::string& str, int base, std::size_t* size); bool ToSize(const std::string& str, int base, std::size_t* size);

Loading…
Cancel
Save