Refine REST support; add demo for REST client and server.

master
Adam Gu 7 years ago
parent e04565a468
commit c557430b8e

@ -14,4 +14,6 @@ if(WEBCC_ENABLE_DEMO)
add_subdirectory(demo/soap/calc_client)
add_subdirectory(demo/soap/calc_server)
endif()
add_subdirectory(demo/rest/book_client)
add_subdirectory(demo/rest/book_server)
endif()

@ -0,0 +1,5 @@
file(GLOB SRCS *.cc *.h)
add_executable(rest_book_client ${SRCS})
target_link_libraries(rest_book_client webcc)

@ -0,0 +1,2 @@
#include "book_client.h"

@ -0,0 +1,6 @@
#ifndef BOOK_CLIENT_H_
#define BOOK_CLIENT_H_
#include <string>
#endif // BOOK_CLIENT_H_

@ -0,0 +1,88 @@
#include <iostream>
#include "boost/lexical_cast.hpp"
#include "webcc/http_client.h"
#include "webcc/http_request.h"
#include "webcc/http_response.h"
class BookListClient {
public:
BookListClient() {
host_ = "localhost";
port_ = "8080";
}
bool ListBooks() {
webcc::HttpRequest http_request;
http_request.set_method(webcc::kHttpGet);
http_request.set_url("/books");
http_request.SetHost(host_, port_);
http_request.Build();
webcc::HttpResponse http_response;
webcc::HttpClient http_client;
webcc::Error error = http_client.SendRequest(http_request, &http_response);
if (error != webcc::kNoError) {
return false;
}
std::cout << "Book list: " << std::endl
<< http_response.content() << std::endl;
return true;
}
private:
std::string host_;
std::string port_;
};
class BookDetailClient {
public:
BookDetailClient() {
host_ = "localhost";
port_ = "8080";
}
bool GetBook(const std::string& id) {
webcc::HttpRequest http_request;
http_request.set_method(webcc::kHttpGet);
http_request.set_url("/books/" + id);
http_request.SetHost(host_, port_);
http_request.Build();
webcc::HttpResponse http_response;
webcc::HttpClient http_client;
webcc::Error error = http_client.SendRequest(http_request, &http_response);
if (error != webcc::kNoError) {
return false;
}
std::cout << "Book: " << id << std::endl
<< http_response.content() << std::endl;
return true;
}
private:
std::string host_;
std::string port_;
};
int main() {
BookListClient book_list_client;
book_list_client.ListBooks();
BookDetailClient book_detail_client;
book_detail_client.GetBook("1");
return 0;
}

@ -0,0 +1,5 @@
file(GLOB SRCS *.cc *.h)
add_executable(rest_book_server ${SRCS})
target_link_libraries(rest_book_server webcc)

@ -0,0 +1,138 @@
#include "book_services.h"
#include <list>
#include "boost/lexical_cast.hpp"
////////////////////////////////////////////////////////////////////////////////
class Book {
public:
std::string id;
std::string title;
double price;
bool IsNull() const {
return id.empty();
}
};
static const Book kNullBook{};
class BookStore {
public:
BookStore() {
books_.push_back({ "1", "Title1", 11.1 });
books_.push_back({ "2", "Title2", 22.2 });
books_.push_back({ "3", "Title3", 33.3 });
}
const std::list<Book>& books() const {
return books_;
}
const Book& GetBook(const std::string& id) const {
auto it = std::find_if(books_.begin(),
books_.end(),
[&id](const Book& book) { return book.id == id; });
if (it == books_.end()) {
return kNullBook;
}
return *it;
}
bool AddBook(const Book& new_book) {
auto it = std::find_if(
books_.begin(),
books_.end(),
[&new_book](const Book& book) { return book.id == new_book.id; });
if (it != books_.end()) {
return false;
}
books_.push_back(new_book);
return true;
}
bool DeleteBook(const std::string& id) {
auto it = std::find_if(books_.begin(),
books_.end(),
[&id](const Book& book) { return book.id == id; });
if (it == books_.end()) {
return false;
}
books_.erase(it);
return true;
}
private:
std::list<Book> books_;
};
static BookStore g_book_store;
////////////////////////////////////////////////////////////////////////////////
// Naively create JSON object string for a book.
// You should use real JSON library in your product code.
static std::string CreateBookJson(const Book& book) {
std::string json = "{ ";
json += "\"id\": " + book.id + ", ";
json += "\"title\": " + book.title + ", ";
json += "\"price\": " + std::to_string(book.price);
json += " }";
return json;
}
bool BookListService::Handle(const std::string& http_method,
const std::vector<std::string>& url_sub_matches,
const std::string& request_content,
std::string* response_content) {
if (http_method == webcc::kHttpGet) {
*response_content = "{ ";
for (const Book& book : g_book_store.books()) {
*response_content += CreateBookJson(book);
*response_content += ",";
}
*response_content += " }";
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
bool BookDetailService::Handle(const std::string& http_method,
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];
if (http_method == webcc::kHttpGet) {
const Book& book = g_book_store.GetBook(book_id);
if (book.IsNull()) {
return false;
}
*response_content = CreateBookJson(book);
return true;
} else if (http_method == webcc::kHttpPost) {
} else if (http_method == webcc::kHttpDelete) {
}
return false;
}

@ -0,0 +1,44 @@
#ifndef BOOK_SERVICES_H_
#define BOOK_SERVICES_H_
#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
// 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<std::string>& url_sub_matches,
const std::string& request_content,
std::string* response_content) override;
};
// 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<std::string>& url_sub_matches,
const std::string& request_content,
std::string* response_content) override;
};
#endif // BOOK_SERVICE_H_

@ -0,0 +1,38 @@
#include <iostream>
#include "webcc/rest_server.h"
#include "book_services.h"
static void Help(const char* argv0) {
std::cout << "Usage: " << argv0 << " <port>" << std::endl;
std::cout << " E.g.," << std::endl;
std::cout << " " << argv0 << " 8080" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
Help(argv[0]);
return 1;
}
unsigned short port = std::atoi(argv[1]);
std::size_t workers = 2;
try {
webcc::RestServer server(port, workers);
server.RegisterService(std::make_shared<BookListService>(),
"/books");
server.RegisterService(std::make_shared<BookDetailService>(),
"/books/(\\d+)");
server.Run();
} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
return 1;
}
return 0;
}

@ -28,7 +28,7 @@ public:
start_line_ = start_line;
}
size_t content_length() const {
std::size_t content_length() const {
return content_length_;
}
@ -43,7 +43,7 @@ public:
SetHeader(kContentType, content_type);
}
void SetContentLength(size_t content_length) {
void SetContentLength(std::size_t content_length) {
content_length_ = content_length;
SetHeader(kContentLength, std::to_string(content_length));
}
@ -53,7 +53,7 @@ public:
content_ = std::move(content);
}
void AppendContent(const char* data, size_t count) {
void AppendContent(const char* data, std::size_t count) {
content_.append(data, count);
}
@ -62,8 +62,7 @@ public:
}
bool IsContentFull() const {
assert(IsContentLengthValid());
return content_.length() >= content_length_;
return IsContentLengthValid() && content_.length() >= content_length_;
}
bool IsContentLengthValid() const {

@ -10,11 +10,12 @@ namespace webcc {
HttpParser::HttpParser(HttpMessage* message)
: message_(message)
, start_line_parsed_(false)
, content_length_parsed_(false)
, header_parsed_(false)
, finished_(false) {
}
Error HttpParser::Parse(const char* data, size_t len) {
Error HttpParser::Parse(const char* data, std::size_t len) {
if (header_parsed_) {
// Add the data to the content.
message_->AppendContent(data, len);
@ -28,10 +29,10 @@ Error HttpParser::Parse(const char* data, size_t len) {
}
pending_data_.append(data, len);
size_t off = 0;
std::size_t off = 0;
while (true) {
size_t pos = pending_data_.find("\r\n", off);
std::size_t pos = pending_data_.find("\r\n", off);
if (pos == std::string::npos) {
break;
}
@ -52,8 +53,8 @@ Error HttpParser::Parse(const char* data, size_t len) {
}
} else {
// Currently, only Content-Length is important to us.
// Other fields are ignored.
if (!message_->IsContentLengthValid()) {
// Other header fields are ignored.
if (!content_length_parsed_) {
ParseContentLength(line);
}
}
@ -64,9 +65,16 @@ Error HttpParser::Parse(const char* data, size_t len) {
if (header_parsed_) {
// Headers just ended.
if (!message_->IsContentLengthValid()) {
// No Content-Length?
return kHttpContentLengthError;
if (!content_length_parsed_) {
// No Content-Length, no content.
message_->SetContentLength(0);
finished_ = true;
return kNoError;
} else {
// Invalid Content-Length in the request.
if (!message_->IsContentLengthValid()) {
return kHttpContentLengthError;
}
}
message_->AppendContent(pending_data_.substr(off));
@ -84,7 +92,7 @@ Error HttpParser::Parse(const char* data, size_t len) {
}
void HttpParser::ParseContentLength(const std::string& line) {
size_t pos = line.find(':');
std::size_t pos = line.find(':');
if (pos == std::string::npos) {
return;
}
@ -92,6 +100,8 @@ void HttpParser::ParseContentLength(const std::string& line) {
std::string name = line.substr(0, pos);
if (boost::iequals(name, kContentLength)) {
content_length_parsed_ = true;
++pos; // Skip ':'.
while (line[pos] == ' ') { // Skip spaces.
++pos;
@ -100,9 +110,9 @@ void HttpParser::ParseContentLength(const std::string& line) {
std::string value = line.substr(pos);
try {
message_->SetContentLength(boost::lexical_cast<size_t>(value));
std::size_t length = boost::lexical_cast<std::size_t>(value);
message_->SetContentLength(length);
} catch (boost::bad_lexical_cast&) {
// TODO
}
}
}

@ -17,7 +17,7 @@ public:
return finished_;
}
Error Parse(const char* data, size_t len);
Error Parse(const char* data, std::size_t len);
protected:
// Parse HTTP start line.
@ -36,6 +36,7 @@ protected:
// Parsing helper flags.
bool start_line_parsed_;
bool content_length_parsed_;
bool header_parsed_;
bool finished_;
};

@ -29,7 +29,7 @@ void HttpRequest::SetHost(const std::string& host, const std::string& port) {
}
}
void HttpRequest::MakeStartLine() {
void HttpRequest::Build() {
if (start_line_.empty()) {
start_line_ = method_;
start_line_ += " ";
@ -48,7 +48,6 @@ const char CRLF[] = { '\r', '\n' };
// ATTENTION: The buffers don't hold the memory!
std::vector<boost::asio::const_buffer> HttpRequest::ToBuffers() const {
assert(!start_line_.empty());
assert(IsContentLengthValid());
std::vector<boost::asio::const_buffer> buffers;
@ -63,7 +62,9 @@ std::vector<boost::asio::const_buffer> HttpRequest::ToBuffers() const {
buffers.push_back(boost::asio::buffer(misc_strings::CRLF));
buffers.push_back(boost::asio::buffer(content_));
if (content_length_ > 0) {
buffers.push_back(boost::asio::buffer(content_));
}
return buffers;
}

@ -51,13 +51,13 @@ public:
// \param port Numeric port number, "80" will be used if it's empty.
void SetHost(const std::string& host, const std::string& port);
// Compose start line from method, url, etc.
void MakeStartLine();
// Compose start line, etc.
// Must be called before ToBuffers()!
void Build();
// Convert the response into a vector of buffers. The buffers do not own the
// underlying memory blocks, therefore the request object must remain valid
// and not be changed until the write operation has completed.
// NOTE: Please call MakeStartLine() before.
std::vector<boost::asio::const_buffer> ToBuffers() const;
private:

@ -87,6 +87,7 @@ HttpStatus::Enum RestRequestHandler::HandleSession(HttpSessionPtr session) {
// TODO: Error handling.
std::string content;
service->Handle(session->request().method(),
sub_matches,
session->request().content(),
&content);

@ -1,8 +1,9 @@
#ifndef WEBCC_REST_SERVICE_H_
#define WEBCC_REST_SERVICE_H_
#include <string>
#include <memory>
#include <string>
#include <vector>
#include "webcc/common.h"
@ -18,8 +19,9 @@ public:
// Both the request and response parameters should be JSON.
// TODO: Query parameters.
virtual bool Handle(const std::string& http_method,
const std::string& request,
std::string* response) = 0;
const std::vector<std::string>& url_sub_matches,
const std::string& request_content,
std::string* response_content) = 0;
};
typedef std::shared_ptr<RestService> RestServicePtr;

Loading…
Cancel
Save