From 7f692a960290c87bfd5cebf13fe7a4f804e5f142 Mon Sep 17 00:00:00 2001 From: Chunting Gu Date: Tue, 28 Aug 2018 13:03:25 +0800 Subject: [PATCH] Refine REST book examples. --- example/common/book_json.cc | 23 ++++--- example/common/book_json.h | 1 + example/rest_book_client/main.cc | 97 +++++++++++++++++++++------- example/rest_book_server/services.cc | 55 +++++++--------- example/rest_book_server/services.h | 30 ++++----- webcc/rest_client.h | 12 ++-- webcc/rest_service.h | 13 ++-- 7 files changed, 138 insertions(+), 93 deletions(-) diff --git a/example/common/book_json.cc b/example/common/book_json.cc index c00e7f8..5b18482 100644 --- a/example/common/book_json.cc +++ b/example/common/book_json.cc @@ -26,11 +26,19 @@ Json::Value StringToJson(const std::string& str) { } Json::Value BookToJson(const Book& book) { - Json::Value root; - root["id"] = book.id; - root["title"] = book.title; - root["price"] = book.price; - return root; + Json::Value json; + json["id"] = book.id; + json["title"] = book.title; + json["price"] = book.price; + return json; +} + +Book JsonToBook(const Json::Value& json) { + return { + json["id"].asString(), + json["title"].asString(), + json["price"].asDouble(), + }; } std::string BookToJsonString(const Book& book) { @@ -44,9 +52,6 @@ bool JsonStringToBook(const std::string& json_str, Book* book) { return false; } - book->id = json["id"].asString(); - book->title = json["title"].asString(); - book->price = json["price"].asDouble(); - + *book = JsonToBook(json); return true; } diff --git a/example/common/book_json.h b/example/common/book_json.h index 9044edb..dbb7027 100644 --- a/example/common/book_json.h +++ b/example/common/book_json.h @@ -12,6 +12,7 @@ std::string JsonToString(const Json::Value& json); Json::Value StringToJson(const std::string& str); Json::Value BookToJson(const Book& book); +Book JsonToBook(const Json::Value& json); std::string BookToJsonString(const Book& book); bool JsonStringToBook(const std::string& json_str, Book* book); diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index e2692e3..4cfae18 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -1,4 +1,5 @@ #include +#include #include "json/json.h" @@ -34,18 +35,24 @@ class BookClientBase { virtual ~BookClientBase() = default; protected: + // Log the socket communication error. void LogError() { if (rest_client_.timed_out()) { LOG_ERRO("%s (timed out)", webcc::DescribeError(rest_client_.error())); } else { LOG_ERRO(webcc::DescribeError(rest_client_.error())); } + } - //std::cout << webcc::DescribeError(rest_client_.error()); - //if (rest_client_.timed_out()) { - // std::cout << " (timed out)"; - //} - //std::cout << std::endl; + // Check HTTP response status. + bool CheckStatus(webcc::HttpStatus::Enum expected_status) { + int status = rest_client_.response_status(); + if (status != expected_status) { + LOG_ERRO("HTTP status error (actual: %d, expected: %d).", + status, expected_status); + return false; + } + return true; } webcc::RestClient rest_client_; @@ -54,19 +61,34 @@ class BookClientBase { // ----------------------------------------------------------------------------- class BookListClient : public BookClientBase { -public: + public: BookListClient(const std::string& host, const std::string& port, int timeout_seconds) : BookClientBase(host, port, timeout_seconds) { } - bool ListBooks() { + bool ListBooks(std::list* books) { if (!rest_client_.Get("/books")) { + // Socket communication error. LogError(); return false; } - std::cout << rest_client_.response_content() << std::endl; + if (!CheckStatus(webcc::HttpStatus::kOK)) { + // Response HTTP status error. + return false; + } + + Json::Value rsp_json = StringToJson(rest_client_.response_content()); + + if (!rsp_json.isArray()) { + return false; // Should be a JSON array of books. + } + + for (Json::ArrayIndex i = 0; i < rsp_json.size(); ++i) { + books->push_back(JsonToBook(rsp_json[i])); + } + return true; } @@ -80,6 +102,10 @@ public: return false; } + if (!CheckStatus(webcc::HttpStatus::kCreated)) { + return false; + } + Json::Value rsp_json = StringToJson(rest_client_.response_content()); *id = rsp_json["id"].asString(); @@ -90,7 +116,7 @@ public: // ----------------------------------------------------------------------------- class BookDetailClient : public BookClientBase { -public: + public: BookDetailClient(const std::string& host, const std::string& port, int timeout_seconds) : BookClientBase(host, port, timeout_seconds) { @@ -102,12 +128,15 @@ public: return false; } + if (!CheckStatus(webcc::HttpStatus::kOK)) { + return false; + } + return JsonStringToBook(rest_client_.response_content(), book); } bool UpdateBook(const std::string& id, const std::string& title, double price) { - // NOTE: ID is already in the URL. Json::Value json; json["title"] = title; json["price"] = price; @@ -117,9 +146,7 @@ public: return false; } - int status = rest_client_.response_status(); - if (status != webcc::HttpStatus::kOK) { - LOG_ERRO("Failed to update book (status: %d).", status); + if (!CheckStatus(webcc::HttpStatus::kOK)) { return false; } @@ -127,14 +154,12 @@ public: } bool DeleteBook(const std::string& id) { - if (!rest_client_.Delete("/books/0" /*+ id*/)) { + if (!rest_client_.Delete("/books/" + id)) { LogError(); return false; } - int status = rest_client_.response_status(); - if (status != webcc::HttpStatus::kOK) { - LOG_ERRO("Failed to delete book (status: %d).", status); + if (!CheckStatus(webcc::HttpStatus::kOK)) { return false; } @@ -148,6 +173,19 @@ void PrintSeparator() { std::cout << std::string(80, '-') << std::endl; } +void PrintBook(const Book& book) { + std::cout << "Book: " << book << std::endl; +} + +void PrintBookList(const std::list& books) { + std::cout << "Book list: " << books.size() << std::endl; + for (const Book& book : books) { + std::cout << " Book: " << book << std::endl; + } +} + +// ----------------------------------------------------------------------------- + void Help(const char* argv0) { std::cout << "Usage: " << argv0 << " [timeout]" << std::endl; std::cout << " E.g.," << std::endl; @@ -176,7 +214,9 @@ int main(int argc, char* argv[]) { PrintSeparator(); - list_client.ListBooks(); + std::list books; + list_client.ListBooks(&books); + PrintBookList(books); PrintSeparator(); @@ -184,13 +224,19 @@ int main(int argc, char* argv[]) { if (!list_client.CreateBook("1984", 12.3, &id)) { return 1; } + std::cout << "Book ID: " << id << std::endl; + + PrintSeparator(); + + books.clear(); + list_client.ListBooks(&books); + PrintBookList(books); PrintSeparator(); Book book; - if (detail_client.GetBook(id, &book)) { - std::cout << "Book: " << book << std::endl; - } + detail_client.GetBook(id, &book); + PrintBook(book); PrintSeparator(); @@ -198,9 +244,8 @@ int main(int argc, char* argv[]) { PrintSeparator(); - if (detail_client.GetBook(id, &book)) { - std::cout << "Book " << book << std::endl; - } + detail_client.GetBook(id, &book); + PrintBook(book); PrintSeparator(); @@ -208,7 +253,9 @@ int main(int argc, char* argv[]) { PrintSeparator(); - list_client.ListBooks(); + books.clear(); + list_client.ListBooks(&books); + PrintBookList(books); return 0; } diff --git a/example/rest_book_server/services.cc b/example/rest_book_server/services.cc index 8e2b091..a68cc71 100644 --- a/example/rest_book_server/services.cc +++ b/example/rest_book_server/services.cc @@ -14,18 +14,22 @@ static BookStore g_book_store; +static void Sleep(int seconds) { + if (seconds > 0) { + LOG_INFO("Sleep %d seconds...", seconds); + std::this_thread::sleep_for(std::chrono::seconds(seconds)); + } +} + // ----------------------------------------------------------------------------- // Return all books as a JSON array. -// TODO: Support query parameters. 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_)); - } + Sleep(sleep_seconds_); Json::Value json(Json::arrayValue); + for (const Book& book : g_book_store.books()) { json.append(BookToJson(book)); } @@ -36,10 +40,7 @@ void BookListService::Get(const webcc::UrlQuery& /*query*/, 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_)); - } + Sleep(sleep_seconds_); Book book; if (JsonStringToBook(request_content, &book)) { @@ -58,17 +59,15 @@ void BookListService::Post(const std::string& request_content, // ----------------------------------------------------------------------------- -void BookDetailService::Get(const std::vector& url_sub_matches, +void BookDetailService::Get(const webcc::UrlSubMatches& url_sub_matches, 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_)); - } + Sleep(sleep_seconds_); if (url_sub_matches.size() != 1) { - // TODO: kNotFound? - response->status = webcc::HttpStatus::kBadRequest; + // Using kNotFound means the resource specified by the URL cannot be found. + // kBadRequest could be another choice. + response->status = webcc::HttpStatus::kNotFound; return; } @@ -84,18 +83,13 @@ void BookDetailService::Get(const std::vector& url_sub_matches, response->status = webcc::HttpStatus::kOK; } -// Update a book. -void BookDetailService::Put(const std::vector& url_sub_matches, +void BookDetailService::Put(const webcc::UrlSubMatches& url_sub_matches, 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_)); - } + Sleep(sleep_seconds_); if (url_sub_matches.size() != 1) { - // TODO: kNotFound? - response->status = webcc::HttpStatus::kBadRequest; + response->status = webcc::HttpStatus::kNotFound; return; } @@ -113,17 +107,12 @@ void BookDetailService::Put(const std::vector& url_sub_matches, response->status = webcc::HttpStatus::kOK; } -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_)); - } +void BookDetailService::Delete(const webcc::UrlSubMatches& url_sub_matches, + webcc::RestResponse* response) { + Sleep(sleep_seconds_); if (url_sub_matches.size() != 1) { - // TODO: kNotFound? - response->status = webcc::HttpStatus::kBadRequest; + response->status = webcc::HttpStatus::kNotFound; return; } diff --git a/example/rest_book_server/services.h b/example/rest_book_server/services.h index 8c4758c..ef48be0 100644 --- a/example/rest_book_server/services.h +++ b/example/rest_book_server/services.h @@ -8,12 +8,6 @@ // ----------------------------------------------------------------------------- -// BookListService handles the HTTP GET and returns the book list based on -// query parameters specified in the URL. -// The URL should be like: -// - /books -// - /books?name={BookName} -// The query parameters could be regular expressions. class BookListService : public webcc::RestListService { public: explicit BookListService(int sleep_seconds) @@ -22,19 +16,17 @@ class BookListService : public webcc::RestListService { protected: // Get a list of books based on query parameters. - // URL examples: - // - /books - // - /books?name={BookName} - void Get(const webcc::UrlQuery& query, - webcc::RestResponse* response) final; + // Support query parameters. + void Get(const webcc::UrlQuery& query, webcc::RestResponse* response) final; // Create a new book. void Post(const std::string& request_content, webcc::RestResponse* response) final; private: - // Sleep for the client to test timeout control. - int sleep_seconds_; + // Sleep some seconds before send back the response. + // For testing timeout control in client side. + int sleep_seconds_; }; // ----------------------------------------------------------------------------- @@ -48,19 +40,23 @@ class BookDetailService : public webcc::RestDetailService { } protected: - void Get(const std::vector& url_sub_matches, + // Get the detailed information of a book. + void Get(const webcc::UrlSubMatches& url_sub_matches, const webcc::UrlQuery& query, webcc::RestResponse* response) final; - void Put(const std::vector& url_sub_matches, + // Update a book. + void Put(const webcc::UrlSubMatches& url_sub_matches, const std::string& request_content, webcc::RestResponse* response) final; - void Delete(const std::vector& url_sub_matches, + // Delete a book. + void Delete(const webcc::UrlSubMatches& url_sub_matches, webcc::RestResponse* response) final; private: - // Sleep for the client to test timeout control. + // Sleep some seconds before send back the response. + // For testing timeout control in client side. int sleep_seconds_; }; diff --git a/webcc/rest_client.h b/webcc/rest_client.h index 2e0f383..11dabeb 100644 --- a/webcc/rest_client.h +++ b/webcc/rest_client.h @@ -24,30 +24,34 @@ class RestClient { timeout_seconds_ = timeout_seconds; } - // Requests - + // HTTP GET request. + // The return value indicates if the socket communication is successful + // or not. If it's failed, check error() and timed_out() for more details. + // For HTTP status, check response_status() instead. inline bool Get(const std::string& url) { return Request(kHttpGet, url, ""); } + // HTTP POST request. inline bool Post(const std::string& url, std::string&& content) { return Request(kHttpPost, url, std::move(content)); } + // HTTP PUT request. inline bool Put(const std::string& url, std::string&& content) { return Request(kHttpPut, url, std::move(content)); } + // HTTP PATCH request. inline bool Patch(const std::string& url, std::string&& content) { return Request(kHttpPatch, url, std::move(content)); } + // HTTP DELETE request. inline bool Delete(const std::string& url) { return Request(kHttpDelete, url, ""); } - // Response & Result - HttpResponsePtr response() const { return response_; } int response_status() const { diff --git a/webcc/rest_service.h b/webcc/rest_service.h index 34e7c4c..f62942d 100644 --- a/webcc/rest_service.h +++ b/webcc/rest_service.h @@ -20,6 +20,9 @@ namespace webcc { // ----------------------------------------------------------------------------- +// Regex sub-matches of the URL. +typedef std::vector UrlSubMatches; + struct RestRequest { // HTTP method (GET, POST, etc.). const std::string& method; @@ -31,7 +34,7 @@ struct RestRequest { const std::string& url_query_str; // Regex sub-matches of the URL (usually resource ID's). - std::vector url_sub_matches; + UrlSubMatches url_sub_matches; }; struct RestResponse { @@ -76,22 +79,22 @@ class RestDetailService : public RestService { void Handle(const RestRequest& request, RestResponse* response) final; protected: - virtual void Get(const std::vector& url_sub_matches, + virtual void Get(const UrlSubMatches& url_sub_matches, const UrlQuery& query, RestResponse* response) { } - virtual void Put(const std::vector& url_sub_matches, + virtual void Put(const UrlSubMatches& url_sub_matches, const std::string& request_content, RestResponse* response) { } - virtual void Patch(const std::vector& url_sub_matches, + virtual void Patch(const UrlSubMatches& url_sub_matches, const std::string& request_content, RestResponse* response) { } - virtual void Delete(const std::vector& url_sub_matches, + virtual void Delete(const UrlSubMatches& url_sub_matches, RestResponse* response) { } };