From a863b379d01ab2638d8e2b3703cfff47124a6634 Mon Sep 17 00:00:00 2001 From: Adam Gu Date: Fri, 24 Aug 2018 16:51:08 +0800 Subject: [PATCH] Refactor RestService interfaces. --- example/rest_book_async_client/main.cc | 22 +---- example/rest_book_client/main.cc | 115 ++++++++++++------------- example/rest_book_server/services.cc | 76 +++++++++------- example/rest_book_server/services.h | 28 +++--- unittest/rest_service_manager_test.cc | 9 +- webcc/rest_request_handler.cc | 46 ++++------ webcc/rest_service.cc | 61 +++++-------- webcc/rest_service.h | 75 ++++++++-------- webcc/url.cc | 50 ++++++----- webcc/url.h | 6 +- 10 files changed, 225 insertions(+), 263 deletions(-) diff --git a/example/rest_book_async_client/main.cc b/example/rest_book_async_client/main.cc index 9beb83a..e7fa6c8 100644 --- a/example/rest_book_async_client/main.cc +++ b/example/rest_book_async_client/main.cc @@ -5,26 +5,8 @@ #include "webcc/logger.h" #include "webcc/rest_async_client.h" -// ----------------------------------------------------------------------------- - -// Write a JSON object to string. -static std::string JsonToString(const Json::Value& json) { - Json::StreamWriterBuilder builder; - return Json::writeString(builder, json); -} - -static Json::Value StringToJson(const std::string& str) { - Json::Value json; - - Json::CharReaderBuilder builder; - std::stringstream stream(str); - std::string errs; - if (!Json::parseFromStream(builder, stream, &json, &errs)) { - std::cerr << errs << std::endl; - } - - return json; -} +#include "example/common/book.h" +#include "example/common/book_json.h" // ----------------------------------------------------------------------------- diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index f4307ef..e2692e3 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -23,26 +23,6 @@ // ----------------------------------------------------------------------------- -static std::string JsonToString(const Json::Value& json) { - Json::StreamWriterBuilder builder; - return Json::writeString(builder, json); -} - -static Json::Value StringToJson(const std::string& str) { - Json::Value json; - - Json::CharReaderBuilder builder; - std::stringstream stream(str); - std::string errs; - if (!Json::parseFromStream(builder, stream, &json, &errs)) { - std::cerr << errs << std::endl; - } - - return json; -} - -// ----------------------------------------------------------------------------- - class BookClientBase { public: BookClientBase(const std::string& host, const std::string& port, @@ -54,18 +34,18 @@ class BookClientBase { virtual ~BookClientBase() = default; protected: - void PrintSeparateLine() { - std::cout << "--------------------------------"; - std::cout << "--------------------------------"; - std::cout << std::endl; - } - - void PrintError() { - std::cout << webcc::DescribeError(rest_client_.error()); + void LogError() { if (rest_client_.timed_out()) { - std::cout << " (timed out)"; + LOG_ERRO("%s (timed out)", webcc::DescribeError(rest_client_.error())); + } else { + LOG_ERRO(webcc::DescribeError(rest_client_.error())); } - std::cout << std::endl; + + //std::cout << webcc::DescribeError(rest_client_.error()); + //if (rest_client_.timed_out()) { + // std::cout << " (timed out)"; + //} + //std::cout << std::endl; } webcc::RestClient rest_client_; @@ -81,11 +61,8 @@ public: } bool ListBooks() { - PrintSeparateLine(); - std::cout << "ListBooks" << std::endl; - if (!rest_client_.Get("/books")) { - PrintError(); + LogError(); return false; } @@ -94,20 +71,15 @@ public: } bool CreateBook(const std::string& title, double price, std::string* id) { - PrintSeparateLine(); - std::cout << "CreateBook: " << title << ", " << price << std::endl; - - Json::Value req_json(Json::objectValue); + Json::Value req_json; req_json["title"] = title; req_json["price"] = price; if (!rest_client_.Post("/books", JsonToString(req_json))) { - PrintError(); + LogError(); return false; } - std::cout << rest_client_.response_status() << std::endl; - Json::Value rsp_json = StringToJson(rest_client_.response_content()); *id = rsp_json["id"].asString(); @@ -125,11 +97,8 @@ public: } bool GetBook(const std::string& id, Book* book) { - PrintSeparateLine(); - std::cout << "GetBook: " << id << std::endl; - if (!rest_client_.Get("/books/" + id)) { - PrintError(); + LogError(); return false; } @@ -138,40 +107,47 @@ public: bool UpdateBook(const std::string& id, const std::string& title, double price) { - PrintSeparateLine(); - std::cout << "UpdateBook: " << id << ", " << title << ", " << price - << std::endl; - // NOTE: ID is already in the URL. - Json::Value json(Json::objectValue); + Json::Value json; json["title"] = title; json["price"] = price; if (!rest_client_.Put("/books/" + id, JsonToString(json))) { - PrintError(); + LogError(); + return false; + } + + int status = rest_client_.response_status(); + if (status != webcc::HttpStatus::kOK) { + LOG_ERRO("Failed to update book (status: %d).", status); return false; } - std::cout << rest_client_.response_status() << std::endl; return true; } bool DeleteBook(const std::string& id) { - PrintSeparateLine(); - std::cout << "DeleteBook: " << id << std::endl; + if (!rest_client_.Delete("/books/0" /*+ id*/)) { + LogError(); + return false; + } - if (!rest_client_.Delete("/books/" + id)) { - PrintError(); + int status = rest_client_.response_status(); + if (status != webcc::HttpStatus::kOK) { + LOG_ERRO("Failed to delete book (status: %d).", status); return false; } - std::cout << rest_client_.response_status() << std::endl; return true; } }; // ----------------------------------------------------------------------------- +void PrintSeparator() { + std::cout << std::string(80, '-') << std::endl; +} + void Help(const char* argv0) { std::cout << "Usage: " << argv0 << " [timeout]" << std::endl; std::cout << " E.g.," << std::endl; @@ -198,21 +174,40 @@ int main(int argc, char* argv[]) { BookListClient list_client(host, port, timeout_seconds); BookDetailClient detail_client(host, port, timeout_seconds); + PrintSeparator(); + list_client.ListBooks(); + PrintSeparator(); + std::string id; - list_client.CreateBook("1984", 12.3, &id); + if (!list_client.CreateBook("1984", 12.3, &id)) { + return 1; + } + + PrintSeparator(); Book book; if (detail_client.GetBook(id, &book)) { - std::cout << "Book " << id << ": " << book << std::endl; + std::cout << "Book: " << book << std::endl; } + PrintSeparator(); + detail_client.UpdateBook(id, "1Q84", 32.1); - detail_client.GetBook(id, &book); + + PrintSeparator(); + + if (detail_client.GetBook(id, &book)) { + std::cout << "Book " << book << std::endl; + } + + PrintSeparator(); detail_client.DeleteBook(id); + PrintSeparator(); + list_client.ListBooks(); return 0; diff --git a/example/rest_book_server/services.cc b/example/rest_book_server/services.cc index 1e18523..8e2b091 100644 --- a/example/rest_book_server/services.cc +++ b/example/rest_book_server/services.cc @@ -18,8 +18,8 @@ static BookStore g_book_store; // Return all books as a JSON array. // TODO: Support query parameters. -bool BookListService::Get(const webcc::UrlQuery& /*query*/, - std::string* response_content) { +void BookListService::Get(const webcc::UrlQuery& /*query*/, + webcc::RestResponse* response) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); @@ -30,14 +30,12 @@ bool BookListService::Get(const webcc::UrlQuery& /*query*/, json.append(BookToJson(book)); } - *response_content = JsonToString(json); - - return true; + response->content = JsonToString(json); + response->status = webcc::HttpStatus::kOK; } -// Add a new book. -bool BookListService::Post(const std::string& request_content, - std::string* response_content) { +void BookListService::Post(const std::string& request_content, + webcc::RestResponse* response) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); @@ -50,75 +48,91 @@ bool BookListService::Post(const std::string& request_content, Json::Value json; json["id"] = id; - *response_content = JsonToString(json); - - return true; + response->content = JsonToString(json); + response->status = webcc::HttpStatus::kCreated; + } else { + // Invalid JSON + response->status = webcc::HttpStatus::kBadRequest; } - - return false; } // ----------------------------------------------------------------------------- -bool BookDetailService::Get(const std::vector& url_sub_matches, +void BookDetailService::Get(const std::vector& url_sub_matches, const webcc::UrlQuery& query, - std::string* response_content) { + webcc::RestResponse* response) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } if (url_sub_matches.size() != 1) { - return false; + // TODO: kNotFound? + response->status = webcc::HttpStatus::kBadRequest; + return; } const std::string& book_id = url_sub_matches[0]; const Book& book = g_book_store.GetBook(book_id); - if (!book.IsNull()) { - *response_content = BookToJsonString(book); - return true; + if (book.IsNull()) { + response->status = webcc::HttpStatus::kNotFound; + return; } - return false; + response->content = BookToJsonString(book); + response->status = webcc::HttpStatus::kOK; } // Update a book. -bool BookDetailService::Put(const std::vector& url_sub_matches, +void BookDetailService::Put(const std::vector& url_sub_matches, const std::string& request_content, - std::string* response_content) { + webcc::RestResponse* response) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } if (url_sub_matches.size() != 1) { - return false; + // TODO: kNotFound? + response->status = webcc::HttpStatus::kBadRequest; + return; } const std::string& book_id = url_sub_matches[0]; Book book; - if (JsonStringToBook(request_content, &book)) { - book.id = book_id; - return g_book_store.UpdateBook(book); + if (!JsonStringToBook(request_content, &book)) { + response->status = webcc::HttpStatus::kBadRequest; + return; } - return false; + book.id = book_id; + g_book_store.UpdateBook(book); + + response->status = webcc::HttpStatus::kOK; } -bool BookDetailService::Delete( - const std::vector& url_sub_matches) { +void BookDetailService::Delete( + const std::vector& url_sub_matches, + webcc::RestResponse* response) { if (sleep_seconds_ > 0) { LOG_INFO("Sleep %d seconds...", sleep_seconds_); std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds_)); } if (url_sub_matches.size() != 1) { - return false; + // TODO: kNotFound? + response->status = webcc::HttpStatus::kBadRequest; + return; } const std::string& book_id = url_sub_matches[0]; - return g_book_store.DeleteBook(book_id); + if (!g_book_store.DeleteBook(book_id)) { + response->status = webcc::HttpStatus::kNotFound; + return; + } + + response->status = webcc::HttpStatus::kOK; } diff --git a/example/rest_book_server/services.h b/example/rest_book_server/services.h index 437a2d2..8c4758c 100644 --- a/example/rest_book_server/services.h +++ b/example/rest_book_server/services.h @@ -16,24 +16,25 @@ // The query parameters could be regular expressions. class BookListService : public webcc::RestListService { public: - explicit BookListService(int sleep_seconds) : sleep_seconds_(sleep_seconds) { + explicit BookListService(int sleep_seconds) + : sleep_seconds_(sleep_seconds) { } protected: - // Return a list of books based on query parameters. + // Get a list of books based on query parameters. // URL examples: // - /books // - /books?name={BookName} - bool Get(const webcc::UrlQuery& query, - std::string* response_content) override; + void Get(const webcc::UrlQuery& query, + webcc::RestResponse* response) final; // Create a new book. - bool Post(const std::string& request_content, - std::string* response_content) override; + void Post(const std::string& request_content, + webcc::RestResponse* response) final; private: // Sleep for the client to test timeout control. - int sleep_seconds_ = 0; + int sleep_seconds_; }; // ----------------------------------------------------------------------------- @@ -47,19 +48,20 @@ class BookDetailService : public webcc::RestDetailService { } protected: - bool Get(const std::vector& url_sub_matches, + void Get(const std::vector& url_sub_matches, const webcc::UrlQuery& query, - std::string* response_content) override; + webcc::RestResponse* response) final; - bool Put(const std::vector& url_sub_matches, + void Put(const std::vector& url_sub_matches, const std::string& request_content, - std::string* response_content) override; + webcc::RestResponse* response) final; - bool Delete(const std::vector& url_sub_matches) override; + void Delete(const std::vector& url_sub_matches, + webcc::RestResponse* response) final; private: // Sleep for the client to test timeout control. - int sleep_seconds_ = 0; + int sleep_seconds_; }; #endif // EXAMPLE_REST_BOOK_SERVER_SERVICES_H_ diff --git a/unittest/rest_service_manager_test.cc b/unittest/rest_service_manager_test.cc index 1b72cde..c9c6f73 100644 --- a/unittest/rest_service_manager_test.cc +++ b/unittest/rest_service_manager_test.cc @@ -3,12 +3,9 @@ class TestRestService : public webcc::RestService { public: - bool Handle(const std::string& http_method, - const std::vector& url_sub_matches, - const webcc::UrlQuery& query, - const std::string& request_content, - std::string* response_content) override { - return true; + void Handle(const webcc::RestRequest& request, + webcc::RestResponse* response) final { + response->status = webcc::HttpStatus::kOK; } }; diff --git a/webcc/rest_request_handler.cc b/webcc/rest_request_handler.cc index ab8b7c5..68a1c4a 100644 --- a/webcc/rest_request_handler.cc +++ b/webcc/rest_request_handler.cc @@ -14,47 +14,39 @@ bool RestRequestHandler::Bind(RestServicePtr service, const std::string& url, } void RestRequestHandler::HandleConnection(HttpConnectionPtr connection) { - const HttpRequest& request = connection->request(); + const HttpRequest& http_request = connection->request(); - Url url(request.url(), true); + Url url(http_request.url(), /*decode*/true); if (!url.IsValid()) { connection->SendResponse(HttpStatus::kBadRequest); return; } - std::vector sub_matches; - RestServicePtr service = service_manager_.GetService(url.path(), - &sub_matches); - if (!service) { - LOG_WARN("No service matches the URL: %s", url.path().c_str()); - connection->SendResponse(HttpStatus::kBadRequest); - return; - } + RestRequest rest_request{ + http_request.method(), http_request.content(), url.query() + }; - UrlQuery query; - if (request.method() == kHttpGet) { - // Suppose URL query is only available for HTTP GET. - Url::SplitQuery(url.query(), &query); - } + // Get service by URL path. + RestServicePtr service = service_manager_.GetService( + url.path(), &rest_request.url_sub_matches); - std::string content; - bool ok = service->Handle(request.method(), sub_matches, query, - request.content(), &content); - if (!ok) { - connection->SendResponse(HttpStatus::kBadRequest); + if (!service) { + LOG_WARN("No service matches the URL path: %s", url.path().c_str()); + connection->SendResponse(HttpStatus::kNotFound); return; } - if (!content.empty()) { - connection->SetResponseContent(std::move(content), kAppJsonUtf8); - } + RestResponse rest_response; + service->Handle(rest_request, &rest_response); - if (request.method() == kHttpPost) { - connection->SendResponse(HttpStatus::kCreated); - } else { - connection->SendResponse(HttpStatus::kOK); + if (!rest_response.content.empty()) { + connection->SetResponseContent(std::move(rest_response.content), + kAppJsonUtf8); } + + // Send response back to client. + connection->SendResponse(rest_response.status); } } // namespace webcc diff --git a/webcc/rest_service.cc b/webcc/rest_service.cc index 1dca6ad..3109ed6 100644 --- a/webcc/rest_service.cc +++ b/webcc/rest_service.cc @@ -6,51 +6,34 @@ namespace webcc { // ----------------------------------------------------------------------------- -bool RestListService::Handle(const std::string& http_method, - const std::vector& url_sub_matches, - const UrlQuery& query, - const std::string& request_content, - std::string* response_content) { - if (http_method == kHttpGet) { - return Get(query, response_content); +void RestListService::Handle(const RestRequest& request, + RestResponse* response) { + if (request.method == kHttpGet) { + Get(UrlQuery(request.url_query_str), response); + } else if (request.method == kHttpPost) { + Post(request.content, response); + } else { + LOG_ERRO("RestListService doesn't support '%s' method.", + request.method.c_str()); } - - if (http_method == kHttpPost) { - return Post(request_content, response_content); - } - - LOG_ERRO("RestListService doesn't support '%s' method.", http_method.c_str()); - - return false; } // ----------------------------------------------------------------------------- -bool RestDetailService::Handle(const std::string& http_method, - const std::vector& url_sub_matches, - const UrlQuery& query, - const std::string& request_content, - std::string* response_content) { - if (http_method == kHttpGet) { - return Get(url_sub_matches, query, response_content); - } - - if (http_method == kHttpPut) { - return Put(url_sub_matches, request_content, response_content); +void RestDetailService::Handle(const RestRequest& request, + RestResponse* response) { + if (request.method == kHttpGet) { + Get(request.url_sub_matches, UrlQuery(request.url_query_str), response); + } else if (request.method == kHttpPut) { + Put(request.url_sub_matches, request.content, response); + } else if (request.method == kHttpPatch) { + Patch(request.url_sub_matches, request.content, response); + } else if (request.method == kHttpDelete) { + Delete(request.url_sub_matches, response); + } else { + LOG_ERRO("RestDetailService doesn't support '%s' method.", + request.method.c_str()); } - - if (http_method == kHttpPatch) { - return Patch(url_sub_matches, request_content, response_content); - } - - if (http_method == kHttpDelete) { - return Delete(url_sub_matches); - } - - LOG_ERRO("RestDetailService doesn't support '%s' method.", - http_method.c_str()); - - return false; } } // namespace webcc diff --git a/webcc/rest_service.h b/webcc/rest_service.h index 375a449..34e7c4c 100644 --- a/webcc/rest_service.h +++ b/webcc/rest_service.h @@ -14,28 +14,40 @@ #include #include "webcc/globals.h" +#include "webcc/url.h" namespace webcc { -class UrlQuery; +// ----------------------------------------------------------------------------- + +struct RestRequest { + // HTTP method (GET, POST, etc.). + const std::string& method; + + // Request content (JSON string). + const std::string& content; + + // Query string of the URL (only for GET). + const std::string& url_query_str; + + // Regex sub-matches of the URL (usually resource ID's). + std::vector url_sub_matches; +}; + +struct RestResponse { + HttpStatus::Enum status; + std::string content; +}; // ----------------------------------------------------------------------------- // Base class for your REST service. class RestService { public: - virtual ~RestService() { - } + virtual ~RestService() = default; - // Handle REST request, output the response. - // The regex sub-matches of the URL (usually resource IDs) were stored in - // |url_sub_matches|. The |query| part of the URL is normally only for GET - // request. Both the request and response contents are JSON strings. - virtual bool Handle(const std::string& http_method, - const std::vector& url_sub_matches, - const UrlQuery& query, - const std::string& request_content, - std::string* response_content) = 0; + // Handle REST request, output response. + virtual void Handle(const RestRequest& request, RestResponse* response) = 0; }; typedef std::shared_ptr RestServicePtr; @@ -44,22 +56,16 @@ typedef std::shared_ptr RestServicePtr; class RestListService : public RestService { public: - bool Handle(const std::string& http_method, - const std::vector& url_sub_matches, - const UrlQuery& query, - const std::string& request_content, - std::string* response_content) final; + void Handle(const RestRequest& request, RestResponse* response) final; protected: RestListService() = default; - virtual bool Get(const UrlQuery& query, std::string* response_content) { - return false; + virtual void Get(const UrlQuery& query, RestResponse* response) { } - virtual bool Post(const std::string& request_content, - std::string* response_content) { - return false; + virtual void Post(const std::string& request_content, + RestResponse* response) { } }; @@ -67,33 +73,26 @@ class RestListService : public RestService { class RestDetailService : public RestService { public: - bool Handle(const std::string& http_method, - const std::vector& url_sub_matches, - const UrlQuery& query, - const std::string& request_content, - std::string* response_content) final; + void Handle(const RestRequest& request, RestResponse* response) final; protected: - virtual bool Get(const std::vector& url_sub_matches, + virtual void Get(const std::vector& url_sub_matches, const UrlQuery& query, - std::string* response_content) { - return false; + RestResponse* response) { } - virtual bool Put(const std::vector& url_sub_matches, + virtual void Put(const std::vector& url_sub_matches, const std::string& request_content, - std::string* response_content) { - return false; + RestResponse* response) { } - virtual bool Patch(const std::vector& url_sub_matches, + virtual void Patch(const std::vector& url_sub_matches, const std::string& request_content, - std::string* response_content) { - return false; + RestResponse* response) { } - virtual bool Delete(const std::vector& url_sub_matches) { - return false; + virtual void Delete(const std::vector& url_sub_matches, + RestResponse* response) { } }; diff --git a/webcc/url.cc b/webcc/url.cc index 8937a99..fa39ea0 100644 --- a/webcc/url.cc +++ b/webcc/url.cc @@ -166,6 +166,30 @@ bool SplitKeyValue(const std::string& kv, std::string* key, // ----------------------------------------------------------------------------- +UrlQuery::UrlQuery(const std::string& str) { + if (!str.empty()) { + // Split into key value pairs separated by '&'. + for (std::size_t i = 0; i != std::string::npos;) { + std::size_t j = str.find_first_of('&', i); + + std::string kv; + if (j == std::string::npos) { + kv = str.substr(i); + i = std::string::npos; + } else { + kv = str.substr(i, j - i); + i = j + 1; + } + + std::string key; + std::string value; + if (SplitKeyValue(kv, &key, &value)) { + Add(std::move(key), std::move(value)); + } + } + } +} + UrlQuery::UrlQuery(const std::map& map) { for (auto& pair : map) { Add(pair.first, pair.second); @@ -256,32 +280,6 @@ std::vector Url::SplitPath(const std::string& path) { return results; } -// static -void Url::SplitQuery(const std::string& str, UrlQuery* query) { - const std::size_t NPOS = std::string::npos; - - // Split into key value pairs separated by '&'. - std::size_t i = 0; - while (i != NPOS) { - std::size_t j = str.find_first_of('&', i); - - std::string kv; - if (j == NPOS) { - kv = str.substr(i); - i = NPOS; - } else { - kv = str.substr(i, j - i); - i = j + 1; - } - - std::string key; - std::string value; - if (SplitKeyValue(kv, &key, &value)) { - query->Add(std::move(key), std::move(value)); - } - } -} - void Url::Init(const std::string& str) { std::size_t pos = str.find('?'); if (pos == std::string::npos) { diff --git a/webcc/url.h b/webcc/url.h index bd6f247..e70885b 100644 --- a/webcc/url.h +++ b/webcc/url.h @@ -24,6 +24,9 @@ class UrlQuery { UrlQuery() = default; + // The query string should be key value pairs separated by '&'. + explicit UrlQuery(const std::string& str); + // Construct from key-value pairs. explicit UrlQuery(const std::map& map); @@ -84,9 +87,6 @@ class Url { // Split a path into its hierarchical components. static std::vector SplitPath(const std::string& path); - // Split query string into key-value parameters. - static void SplitQuery(const std::string& str, UrlQuery* query); - private: void Init(const std::string& str);