Add RestListService and RestDetailService; update README.
This commit is contained in:
@@ -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?<query>
|
||||
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<std::string>& url_sub_matches,
|
||||
std::string* response_content) final;
|
||||
|
||||
// Update the information of a book.
|
||||
bool Patch(const std::vector<std::string>& url_sub_matches,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) final;
|
||||
|
||||
// Delete a book.
|
||||
bool Delete(const std::vector<std::string>& 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<std::string>& 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<BookListService>(),
|
||||
"/books");
|
||||
|
||||
server.RegisterService(std::make_shared<BookDetailService>(),
|
||||
"/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
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::string>& 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());
|
||||
}
|
||||
|
||||
Json::StreamWriterBuilder builder;
|
||||
*response_content = Json::writeString(builder, root);
|
||||
|
||||
return true;
|
||||
// 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());
|
||||
}
|
||||
|
||||
if (http_method == webcc::kHttpPost) {
|
||||
// Add a new book.
|
||||
Book book;
|
||||
if (BookFromJson(request_content, &book)) {
|
||||
return g_book_store.AddBook(book);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Json::StreamWriterBuilder builder;
|
||||
*response_content = Json::writeString(builder, root);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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<std::string>& url_sub_matches,
|
||||
const webcc::UrlQuery& query,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) {
|
||||
bool BookDetailService::Get(const std::vector<std::string>& 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;
|
||||
|
||||
} 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;
|
||||
|
||||
} else if (http_method == webcc::kHttpDelete) {
|
||||
return g_book_store.DeleteBook(book_id);
|
||||
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;
|
||||
}
|
||||
|
||||
// Update a book partially.
|
||||
bool BookDetailService::Patch(const std::vector<std::string>& 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];
|
||||
|
||||
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<std::string>& 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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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;
|
||||
|
||||
bool Handle(const std::string& http_method,
|
||||
const std::vector<std::string>& url_sub_matches,
|
||||
const webcc::UrlQuery& query,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) override;
|
||||
// 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;
|
||||
// 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<std::string>& url_sub_matches,
|
||||
std::string* response_content) final;
|
||||
|
||||
bool Handle(const std::string& http_method,
|
||||
const std::vector<std::string>& url_sub_matches,
|
||||
const webcc::UrlQuery& query,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) override;
|
||||
bool Patch(const std::vector<std::string>& url_sub_matches,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) final;
|
||||
|
||||
bool Delete(const std::vector<std::string>& url_sub_matches) final;
|
||||
};
|
||||
|
||||
#endif // BOOK_SERVICE_H_
|
||||
|
||||
@@ -30,7 +30,7 @@ int main(int argc, char* argv[]) {
|
||||
"/books");
|
||||
|
||||
server.RegisterService(std::make_shared<BookDetailService>(),
|
||||
"/books/(\\d+)");
|
||||
"/book/(\\d+)");
|
||||
|
||||
server.Run();
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ set(SRCS
|
||||
queue.h
|
||||
rest_server.cc
|
||||
rest_server.h
|
||||
rest_service.cc
|
||||
rest_service.h
|
||||
url.cc
|
||||
url.h
|
||||
|
||||
+4
-2
@@ -5,6 +5,8 @@
|
||||
|
||||
#if WEBCC_ENABLE_LOG
|
||||
|
||||
#include <cstring> // 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);
|
||||
|
||||
@@ -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<std::string>& 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<std::string>& 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
|
||||
@@ -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 <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -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<RestService> RestServicePtr;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class RestListService : public RestService {
|
||||
public:
|
||||
bool Handle(const std::string& http_method,
|
||||
const std::vector<std::string>& 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<std::string>& url_sub_matches,
|
||||
const UrlQuery& query,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) final;
|
||||
|
||||
protected:
|
||||
virtual bool Get(const std::vector<std::string>& url_sub_matches,
|
||||
std::string* response_content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool Put(const std::vector<std::string>& url_sub_matches,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool Patch(const std::vector<std::string>& url_sub_matches,
|
||||
const std::string& request_content,
|
||||
std::string* response_content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool Delete(const std::vector<std::string>& url_sub_matches) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_REST_SERVICE_H_
|
||||
|
||||
Reference in New Issue
Block a user