diff --git a/README.md b/README.md index fbcfb5c..3b74cb7 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,95 @@ A lightweight C++ REST and SOAP client and server library based on *Boost.Asio*. -Please see the `doc` folder for tutorials and `example` folder for examples. +## A Quick Look + +### REST Server + +Suppose you want to create a book server, and provide the following operations with RESTful API: + +- Query books based on some criterias. +- Add a new book. +- Get the detailed information of a book. +- Update the information of a book. +- Delete a book. + +The first two operations can be implemented by deriving from `webcc::RestListService`: + +```cpp +class BookListService : public webcc::RestListService { + protected: + // Query books based on some criterias. + // GET /books? + bool Get(const webcc::UrlQuery& query, + std::string* response_content) final; + + // Add a new book. + // POST /books + // The new book's data is attached as request content in JSON format. + bool Post(const std::string& request_content, + std::string* response_content) final; +}; +``` + +The others, derive from `webcc::RestDetailService`: + +```cpp +// The URL is like '/book/{BookID}', and the 'url_sub_matches' parameter +// contains the matched book ID. +class BookDetailService : public webcc::RestDetailService { + protected: + // Get the detailed information of a book. + bool Get(const std::vector& url_sub_matches, + std::string* response_content) final; + + // Update the information of a book. + bool Patch(const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) final; + + // Delete a book. + bool Delete(const std::vector& url_sub_matches) final; +}; +``` + +As you can see, all you have to do is to override the proper virtual functions which are named after HTTP methods. + +The detailed implementation is out of the scope of this document, but here is an example: + +```cpp +bool BookDetailService::Get(const std::vector& url_sub_matches, + std::string* response_content) { + if (url_sub_matches.size() != 1) { + return false; + } + const std::string& book_id = url_sub_matches[0]; + + // Get the book by ID from database. + // Convert the book details to JSON string. + // Assign JSON string to response_content. +} +``` + +Last step, register the services and run the server: + +```cpp +webcc::RestServer server(8080, 2); + +server.RegisterService(std::make_shared(), + "/books"); + +server.RegisterService(std::make_shared(), + "/book/(\\d+)"); + +server.Run(); +``` + +**Please see the `example` folder for the complete examples (including the client).** ## Build Instructions -A lot of C++11 features are used, e.g., `std::move`. But C++14 is not required. It means that you can still build `webcc` using VS2013. +A lot of C++11 features are used, e.g., `std::move`. But C++14 is not required. +(It means that you can still build `webcc` using VS2013 on Windows.) [CMake 3.1.0+](https://cmake.org/) is required as the build system. But if you don't use CMake, you can just copy the `src/webcc` folder to your own project then manage it by yourself. @@ -25,3 +109,26 @@ option(WEBCC_BUILD_SOAP_EXAMPLE "Build SOAP example?" ON) ``` If `WEBCC_ENABLE_SOAP` is `ON`, **pugixml** (already included) is used to parse and compose XML strings. + +### Build On Linux + +Create a build folder under the root (or any other) directory, and `cd` to it: +```bash +mkdir build +cd build +``` +Generate Makefiles with the following command: +```bash +cmake -G"Unix Makefiles" \ + -DWEBCC_ENABLE_LOG=ON \ + -DWEBCC_ENABLE_SOAP=ON \ + -DWEBCC_BUILD_UNITTEST=OFF \ + -DWEBCC_BUILD_REST_EXAMPLE=ON \ + -DWEBCC_BUILD_SOAP_EXAMPLE=OFF \ + .. +``` +Feel free to change the build options (`ON` or `OFF`). +Then make: +```bash +$ make -j4 +``` diff --git a/compile_commands.json b/compile_commands.json index 7ce550f..7490d83 100644 --- a/compile_commands.json +++ b/compile_commands.json @@ -69,6 +69,11 @@ "command": "/usr/bin/c++ -DBOOST_ASIO_NO_DEPRECATED -DWEBCC_ENABLE_LOG -DWEBCC_ENABLE_SOAP -I/home/adam/github/webcc/src -I/home/adam/github/webcc/third_party -std=c++11 -o CMakeFiles/webcc.dir/rest_server.cc.o -c /home/adam/github/webcc/src/webcc/rest_server.cc", "file": "/home/adam/github/webcc/src/webcc/rest_server.cc" }, +{ + "directory": "/home/adam/github/webcc/build/src/webcc", + "command": "/usr/bin/c++ -DBOOST_ASIO_NO_DEPRECATED -DWEBCC_ENABLE_LOG -DWEBCC_ENABLE_SOAP -I/home/adam/github/webcc/src -I/home/adam/github/webcc/third_party -std=c++11 -o CMakeFiles/webcc.dir/rest_service.cc.o -c /home/adam/github/webcc/src/webcc/rest_service.cc", + "file": "/home/adam/github/webcc/src/webcc/rest_service.cc" +}, { "directory": "/home/adam/github/webcc/build/src/webcc", "command": "/usr/bin/c++ -DBOOST_ASIO_NO_DEPRECATED -DWEBCC_ENABLE_LOG -DWEBCC_ENABLE_SOAP -I/home/adam/github/webcc/src -I/home/adam/github/webcc/third_party -std=c++11 -o CMakeFiles/webcc.dir/url.cc.o -c /home/adam/github/webcc/src/webcc/url.cc", diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index b086394..d6e6c15 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -92,7 +92,7 @@ public: bool GetBook(const std::string& id) { webcc::HttpResponse http_response; - if (!Request(webcc::kHttpGet, "/books/" + id, "", &http_response)) { + if (!Request(webcc::kHttpGet, "/book/" + id, "", &http_response)) { return false; } @@ -113,7 +113,7 @@ public: std::string book_json = Json::writeString(builder, root); webcc::HttpResponse http_response; - if (!Request(webcc::kHttpPost, "/books/" + id, book_json, &http_response)) { + if (!Request(webcc::kHttpPost, "/book/" + id, book_json, &http_response)) { return false; } @@ -124,7 +124,7 @@ public: bool DeleteBook(const std::string& id) { webcc::HttpResponse http_response; - if (!Request(webcc::kHttpDelete, "/books/" + id, "", &http_response)) { + if (!Request(webcc::kHttpDelete, "/book/" + id, "", &http_response)) { return false; } diff --git a/example/rest_book_server/book_services.cc b/example/rest_book_server/book_services.cc index 0c3f8e6..ed05116 100644 --- a/example/rest_book_server/book_services.cc +++ b/example/rest_book_server/book_services.cc @@ -115,71 +115,77 @@ static bool BookFromJson(const std::string& json, Book* book) { return true; } -bool BookListService::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) { - if (http_method == webcc::kHttpGet) { - // Return all books as a JSON array. - - Json::Value root(Json::arrayValue); - for (const Book& book : g_book_store.books()) { - root.append(book.ToJson()); - } +// Return all books as a JSON array. +// TODO: Support query parameters. +bool BookListService::Get(const webcc::UrlQuery& /* query */, + std::string* response_content) { + Json::Value root(Json::arrayValue); + for (const Book& book : g_book_store.books()) { + root.append(book.ToJson()); + } - Json::StreamWriterBuilder builder; - *response_content = Json::writeString(builder, root); + Json::StreamWriterBuilder builder; + *response_content = Json::writeString(builder, root); - return true; - } + return true; +} - if (http_method == webcc::kHttpPost) { - // Add a new book. - Book book; - if (BookFromJson(request_content, &book)) { - return g_book_store.AddBook(book); - } - return false; +// Add a new book. +// No response content. +bool BookListService::Post(const std::string& request_content, + std::string* /* response_content */) { + Book book; + if (BookFromJson(request_content, &book)) { + return g_book_store.AddBook(book); } - return false; } //////////////////////////////////////////////////////////////////////////////// -bool BookDetailService::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) { +bool BookDetailService::Get(const std::vector& url_sub_matches, + std::string* response_content) { if (url_sub_matches.size() != 1) { return false; } const std::string& book_id = url_sub_matches[0]; - if (http_method == webcc::kHttpGet) { - const Book& book = g_book_store.GetBook(book_id); - if (!book.IsNull()) { - Json::StreamWriterBuilder builder; - *response_content = Json::writeString(builder, book.ToJson()); - return true; - } - return false; + const Book& book = g_book_store.GetBook(book_id); + if (!book.IsNull()) { + Json::StreamWriterBuilder builder; + *response_content = Json::writeString(builder, book.ToJson()); + return true; + } - } else if (http_method == webcc::kHttpPost) { - // Update a book. - Book book; - if (BookFromJson(request_content, &book)) { - book.id = book_id; - return g_book_store.UpdateBook(book); - } + return false; +} + +// Update a book partially. +bool BookDetailService::Patch(const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) { + if (url_sub_matches.size() != 1) { return false; + } + + const std::string& book_id = url_sub_matches[0]; - } else if (http_method == webcc::kHttpDelete) { - return g_book_store.DeleteBook(book_id); + Book book; + if (BookFromJson(request_content, &book)) { + book.id = book_id; + return g_book_store.UpdateBook(book); } return false; } + +bool BookDetailService::Delete(const std::vector& url_sub_matches) { + if (url_sub_matches.size() != 1) { + return false; + } + + const std::string& book_id = url_sub_matches[0]; + + return g_book_store.DeleteBook(book_id); +} diff --git a/example/rest_book_server/book_services.h b/example/rest_book_server/book_services.h index 24a5c14..0e74ca5 100644 --- a/example/rest_book_server/book_services.h +++ b/example/rest_book_server/book_services.h @@ -3,48 +3,42 @@ #include "webcc/rest_service.h" -// NOTE: -// XxxListService and XxxDetailService are similar to the XxxListView -// and XxxDetailView in Django (a Python web framework). - //////////////////////////////////////////////////////////////////////////////// -// List Service handles the HTTP GET and returns the book list based on +// 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::RestService { -public: - BookListService() = default; - ~BookListService() override = default; - - 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; +class BookListService : public webcc::RestListService { + protected: + // Return a list of books based on query parameters. + // URL examples: + // - /books + // - /books?name={BookName} + bool Get(const webcc::UrlQuery& query, + std::string* response_content) final; + + // Create a new book. + bool Post(const std::string& request_content, + std::string* response_content) final; }; //////////////////////////////////////////////////////////////////////////////// -// Detail Service handles the following HTTP methods: -// - GET -// - PUT -// - PATCH -// - DELETE -// The URL should be like: /books/{BookID}. -class BookDetailService : public webcc::RestService { -public: - BookDetailService() = default; - ~BookDetailService() override = default; - - 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; +// The URL is like '/books/{BookID}', and the 'url_sub_matches' parameter +// contains the matched book ID. +class BookDetailService : public webcc::RestDetailService { + protected: + bool Get(const std::vector& url_sub_matches, + std::string* response_content) final; + + bool Patch(const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) final; + + bool Delete(const std::vector& url_sub_matches) final; }; #endif // BOOK_SERVICE_H_ diff --git a/example/rest_book_server/main.cc b/example/rest_book_server/main.cc index 2cb453e..1f276e0 100644 --- a/example/rest_book_server/main.cc +++ b/example/rest_book_server/main.cc @@ -30,7 +30,7 @@ int main(int argc, char* argv[]) { "/books"); server.RegisterService(std::make_shared(), - "/books/(\\d+)"); + "/book/(\\d+)"); server.Run(); diff --git a/src/webcc/CMakeLists.txt b/src/webcc/CMakeLists.txt index 5d7659d..b16d897 100644 --- a/src/webcc/CMakeLists.txt +++ b/src/webcc/CMakeLists.txt @@ -29,6 +29,7 @@ set(SRCS queue.h rest_server.cc rest_server.h + rest_service.cc rest_service.h url.cc url.h diff --git a/src/webcc/logger.h b/src/webcc/logger.h index 92b4fe3..8cf736e 100644 --- a/src/webcc/logger.h +++ b/src/webcc/logger.h @@ -5,6 +5,8 @@ #if WEBCC_ENABLE_LOG +#include // for strrchr() + namespace webcc { enum LogLevel { @@ -32,7 +34,7 @@ void LogWrite(int level, const char* file, int line, const char* format, ...); #if (defined(WIN32) || defined(_WIN64)) // See: https://stackoverflow.com/a/8488201 -#define __FILENAME__ strrchr("\\" __FILE__, '\\') + 1 +#define __FILENAME__ std::strrchr("\\" __FILE__, '\\') + 1 #define LOG_VERB(format, ...) \ webcc::LogWrite(webcc::VERB, __FILENAME__, __LINE__, format, ##__VA_ARGS__); @@ -52,7 +54,7 @@ void LogWrite(int level, const char* file, int line, const char* format, ...); #else // See: https://stackoverflow.com/a/8488201 -#define __FILENAME__ strrchr("/" __FILE__, '/') + 1 +#define __FILENAME__ std::strrchr("/" __FILE__, '/') + 1 #define LOG_VERB(format, args...) \ webcc::LogWrite(webcc::VERB, __FILENAME__, __LINE__, format, ##args); diff --git a/src/webcc/rest_service.cc b/src/webcc/rest_service.cc new file mode 100644 index 0000000..1704513 --- /dev/null +++ b/src/webcc/rest_service.cc @@ -0,0 +1,53 @@ +#include "webcc/rest_service.h" +#include "webcc/logger.h" + +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); + } + + 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, response_content); + } + + if (http_method == kHttpPut) { + return Put(url_sub_matches, request_content, response_content); + } + + 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/src/webcc/rest_service.h b/src/webcc/rest_service.h index 9b953ff..1f193d5 100644 --- a/src/webcc/rest_service.h +++ b/src/webcc/rest_service.h @@ -1,6 +1,13 @@ #ifndef WEBCC_REST_SERVICE_H_ #define WEBCC_REST_SERVICE_H_ +// NOTE: +// The design of RestListService and RestDetailService is very similar to +// XxxListView and XxxDetailView in Django Rest Framework. +// Deriving from them instead of RestService can simplify your own REST services +// a lot. But if you find the filtered parameters cannot meet your needs, feel +// free to derive from RestService directly. + #include #include #include @@ -12,6 +19,8 @@ namespace webcc { class UrlQuery; +//////////////////////////////////////////////////////////////////////////////// + // Base class for your REST service. class RestService { public: @@ -34,6 +43,63 @@ public: 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; + + protected: + RestListService() = default; + + virtual bool Get(const UrlQuery& query, + std::string* response_content) { + return false; + } + + virtual bool Post(const std::string& request_content, + std::string* response_content) { + return false; + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +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; + + protected: + virtual bool Get(const std::vector& url_sub_matches, + std::string* response_content) { + return false; + } + + virtual bool Put(const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) { + return false; + } + + virtual bool Patch(const std::vector& url_sub_matches, + const std::string& request_content, + std::string* response_content) { + return false; + } + + virtual bool Delete(const std::vector& url_sub_matches) { + return false; + } +}; + } // namespace webcc #endif // WEBCC_REST_SERVICE_H_