Commit 07c3163f authored by octal's avatar octal

Added a DSL to describe a REST API.

This DSL allows API introspection and Swagger integration.
A new Swagger handler can be added to a REST router to
provide a swagger-ui to the user as well as a swagger.json
description file.
parent aa55908d
[submodule "thirdparty/rapidjson"]
path = thirdparty/rapidjson
url = https://github.com/miloyip/rapidjson.git
......@@ -16,6 +16,9 @@ include_directories (${CMAKE_CURRENT_SOURCE_DIR}/include)
file(GLOB HEADER_FILES "include/*.h")
install(FILES ${HEADER_FILES} DESTINATION include/rest)
add_subdirectory (thirdparty)
include_directories(${RapidJSON_SOURCE_DIR}/include)
add_subdirectory (src)
include_directories (src)
......
......@@ -13,3 +13,5 @@ target_link_libraries(custom_header net)
add_executable(rest_server rest_server.cc)
target_link_libraries(rest_server net)
add_executable(rest_description rest_description.cc)
target_link_libraries(rest_description net)
/* rest_description.cc
Mathieu Stefani, 27 février 2016
Example of how to use the Description mechanism
*/
#include "http.h"
#include "description.h"
#include "endpoint.h"
using namespace std;
using namespace Net;
namespace Generic {
void handleReady(const Rest::Request&, Http::ResponseWriter response) {
response.send(Http::Code::Ok, "1");
}
}
class BankerService {
public:
BankerService(Net::Address addr)
: httpEndpoint(std::make_shared<Net::Http::Endpoint>(addr))
, desc("Banking API", "0.1")
{ }
void init(size_t thr = 2) {
auto opts = Net::Http::Endpoint::options()
.threads(thr)
.flags(Net::Tcp::Options::InstallSignalHandler);
httpEndpoint->init(opts);
createDescription();
}
void start() {
router.initFromDescription(desc);
Rest::Swagger swagger(desc);
swagger
.uiPath("/doc")
.uiDirectory("/home/octal/code/web/swagger-ui-2.1.4/dist/")
.apiPath("/banker-api.json")
.install(router);
httpEndpoint->setHandler(router.handler());
httpEndpoint->serve();
}
void shutdown() {
httpEndpoint->shutdown();
}
private:
void createDescription() {
desc
.info()
.license("Apache", "http://www.apache.org/licenses/LICENSE-2.0");
auto backendErrorResponse =
desc.response(Http::Code::Internal_Server_Error, "An error occured with the backend");
desc.schemes("http");
desc
.route(desc.get("/ready"))
.bind(&Generic::handleReady)
.response(Http::Code::Ok, "Response to the /ready call");
auto versionPath = desc.path("/v1");
auto accountsPath = versionPath.path("/accounts");
accountsPath
.route(desc.get("/all"))
.bind(&BankerService::retrieveAllAccounts, this)
.produces(MIME(Application, Json))
.response(Http::Code::Ok, "The list of all account");
accountsPath
.route(desc.get("/:name"), "Retrieve an account")
.bind(&BankerService::retrieveAccount, this)
.produces(MIME(Application, Json))
.parameter<Rest::Type::String>("name", "The name of the account to retrieve")
.response(Http::Code::Ok, "The requested account")
.response(backendErrorResponse);
accountsPath
.route(desc.post("/:name"), "Create an account")
.bind(&BankerService::createAccount, this)
.produces(MIME(Application, Json))
.consumes(MIME(Application, Json))
.parameter<Rest::Type::String>("name", "The name of the account to create")
.response(Http::Code::Ok, "The initial state of the account")
.response(backendErrorResponse);
auto accountPath = accountsPath.path("/:name");
accountPath.parameter<Rest::Type::String>("name", "The name of the account to operate on");
accountPath
.route(desc.post("/budget"), "Add budget to the account")
.bind(&BankerService::creditAccount, this)
.produces(MIME(Application, Json))
.response(Http::Code::Ok, "Budget has been added to the account")
.response(backendErrorResponse);
}
void retrieveAllAccounts(const Rest::Request& req, Http::ResponseWriter response) {
response.send(Http::Code::Ok, "No Account");
}
void retrieveAccount(const Rest::Request& req, Http::ResponseWriter response) {
response.send(Http::Code::Ok, "The bank is closed, come back later");
}
void createAccount(const Rest::Request& req, Http::ResponseWriter response) {
response.send(Http::Code::Ok, "The bank is closed, come back later");
}
void creditAccount(const Rest::Request& req, Http::ResponseWriter response) {
response.send(Http::Code::Ok, "The bank is closed, come back later");
}
std::shared_ptr<Net::Http::Endpoint> httpEndpoint;
Rest::Description desc;
Rest::Router router;
};
int main(int argc, char *argv[]) {
Net::Port port(9080);
int thr = 2;
if (argc >= 2) {
port = std::stol(argv[1]);
if (argc == 3)
thr = std::stol(argv[2]);
}
Net::Address addr(Net::Ipv4::any(), port);
cout << "Cores = " << hardware_concurrency() << endl;
cout << "Using " << thr << " threads" << endl;
BankerService banker(addr);
banker.init(thr);
banker.start();
banker.shutdown();
}
This diff is collapsed.
......@@ -352,6 +352,23 @@ private:
Net::Port port_;
};
class Location : public Header {
public:
NAME("Location")
Location() { }
explicit Location(const std::string& location);
void parse(const std::string& data);
void write(std::ostream& os) const;
std::string location() const { return location_; }
private:
std::string location_;
};
class Server : public Header {
public:
NAME("Server")
......
/*
Mathieu Stefani, 28 février 2016
A collection of sample iterator adapters
*/
#pragma once
template<typename Map>
struct FlatMapIteratorAdapter {
typedef typename Map::key_type Key;
typedef typename Map::mapped_type Value;
typedef typename Map::const_iterator const_iterator;
FlatMapIteratorAdapter(const_iterator it)
: it(it)
{ }
FlatMapIteratorAdapter operator++() {
++it;
return FlatMapIteratorAdapter(it);
}
const Value& operator*() {
return it->second;
}
bool operator==(FlatMapIteratorAdapter other) {
return other.it == it;
}
bool operator!=(FlatMapIteratorAdapter other) {
return !(*this == other);
}
private:
const_iterator it;
};
template<typename Map>
FlatMapIteratorAdapter<Map>
makeFlatMapIterator(const Map&, typename Map::const_iterator it) {
return FlatMapIteratorAdapter<Map>(it);
}
......@@ -16,6 +16,8 @@ namespace Net {
namespace Rest {
class Description;
namespace details {
template<typename T> struct LexicalCast {
static T cast(const std::string& value) {
......@@ -59,7 +61,9 @@ private:
class Request;
struct Route {
typedef std::function<void (const Request&, Net::Http::ResponseWriter)> Handler;
enum class Result { Ok, Failure };
typedef std::function<Result (const Request&, Net::Http::ResponseWriter)> Handler;
Route(std::string resource, Http::Method method, Handler handler)
: resource_(std::move(resource))
......@@ -128,10 +132,42 @@ private:
};
namespace Private {
class RouterHandler;
}
class Router {
public:
enum class Status { Match, NotFound };
static Router fromDescription(const Rest::Description& desc);
std::shared_ptr<Private::RouterHandler>
handler() const;
void initFromDescription(const Rest::Description& desc);
void get(std::string resource, Route::Handler handler);
void post(std::string resource, Route::Handler handler);
void put(std::string resource, Route::Handler handler);
void del(std::string resource, Route::Handler handler);
void addCustomHandler(Route::Handler handler);
Status route(const Http::Request& request, Http::ResponseWriter response);
private:
void addRoute(Http::Method method, std::string resource, Route::Handler handler);
std::unordered_map<Http::Method, std::vector<Route>> routes;
class HttpHandler : public Net::Http::Handler {
std::vector<Route::Handler> customHandlers;
};
namespace Private {
class RouterHandler : public Net::Http::Handler {
public:
HttpHandler(const std::unordered_map<Http::Method, std::vector<Route>>& routes);
RouterHandler(const Rest::Router& router);
void onRequest(
const Http::Request& req,
......@@ -139,16 +175,16 @@ namespace Private {
private:
std::shared_ptr<Net::Tcp::Handler> clone() const {
return std::make_shared<HttpHandler>(routes);
return std::make_shared<RouterHandler>(router);
}
std::unordered_map<Http::Method, std::vector<Route>> routes;
Rest::Router router;
};
}
class Request : public Http::Request {
public:
friend class Private::HttpHandler;
friend class Router;
bool hasParam(std::string name) const;
TypedParam param(std::string name) const;
......@@ -166,23 +202,6 @@ private:
std::vector<TypedParam> splats_;
};
class Router {
public:
std::shared_ptr<Private::HttpHandler>
handler() const {
return std::make_shared<Private::HttpHandler>(routes);
}
void get(std::string resource, Route::Handler handler);
void post(std::string resource, Route::Handler handler);
void put(std::string resource, Route::Handler handler);
void del(std::string resource, Route::Handler handler);
private:
void addRoute(Http::Method method, std::string resource, Route::Handler handler);
std::unordered_map<Http::Method, std::vector<Route>> routes;
};
namespace Routes {
......@@ -225,6 +244,8 @@ namespace Routes {
return [=](const Rest::Request& request, Http::ResponseWriter response) {
CALL_MEMBER_FN(obj, func)(request, std::move(response));
return Route::Result::Ok;
};
#undef CALL_MEMBER_FN
......@@ -236,6 +257,8 @@ namespace Routes {
return [=](const Rest::Request& request, Http::ResponseWriter response) {
func(request, std::move(response));
return Route::Result::Ok;
};
}
......
This diff is collapsed.
......@@ -359,6 +359,20 @@ Host::write(std::ostream& os) const {
}
}
Location::Location(const std::string& location)
: location_(location)
{ }
void
Location::parse(const std::string& data) {
location_ = data;
}
void
Location::write(std::ostream& os) const {
os << location_;
}
void
UserAgent::parse(const std::string& data) {
ua_ = data;
......
......@@ -31,6 +31,7 @@ RegisterHeader(ContentType);
RegisterHeader(Date);
RegisterHeader(Expect);
RegisterHeader(Host);
RegisterHeader(Location);
RegisterHeader(Server);
RegisterHeader(UserAgent);
......
......@@ -5,6 +5,7 @@
*/
#include "router.h"
#include "description.h"
#include <algorithm>
namespace Net {
......@@ -209,34 +210,44 @@ Route::match(const std::string& req) const
namespace Private {
HttpHandler::HttpHandler(
const std::unordered_map<Http::Method, std::vector<Route>>& routes)
: routes(routes)
RouterHandler::RouterHandler(const Rest::Router& router)
: router(router)
{
}
void
HttpHandler::onRequest(
RouterHandler::onRequest(
const Http::Request& req,
Http::ResponseWriter response)
{
auto& r = routes[req.method()];
for (const auto& route: r) {
bool match;
std::vector<TypedParam> params;
std::vector<TypedParam> splats;
std::tie(match, params, splats) = route.match(req);
if (match) {
route.invokeHandler(Request(req, std::move(params), std::move(splats)), std::move(response));
return;
}
}
response.send(Http::Code::Not_Found, "Could not find a matching route");
router.route(req, std::move(response));
}
} // namespace Private
Router
Router::fromDescription(const Rest::Description& desc) {
Router router;
router.initFromDescription(desc);
return router;
}
std::shared_ptr<Private::RouterHandler>
Router::handler() const {
return std::make_shared<Private::RouterHandler>(*this);
}
void
Router::initFromDescription(const Rest::Description& desc) {
auto paths = desc.paths();
for (auto it = paths.flatBegin(), end = paths.flatEnd(); it != end; ++it) {
const auto& paths = *it;
for (const auto& path: paths) {
addRoute(path.method, std::move(path.value), std::move(path.handler));
}
}
}
void
Router::get(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Get, std::move(resource), std::move(handler));
......@@ -257,6 +268,40 @@ Router::del(std::string resource, Route::Handler handler) {
addRoute(Http::Method::Delete, std::move(resource), std::move(handler));
}
void
Router::addCustomHandler(Route::Handler handler) {
customHandlers.push_back(std::move(handler));
}
Router::Status
Router::route(const Http::Request& req, Http::ResponseWriter response) {
auto& r = routes[req.method()];
for (const auto& route: r) {
bool match;
std::vector<TypedParam> params;
std::vector<TypedParam> splats;
std::tie(match, params, splats) = route.match(req);
if (match) {
route.invokeHandler(Request(req, std::move(params), std::move(splats)), std::move(response));
return Router::Status::Match;
}
}
/* @Major @FixMe:
* original response object should not be moved here. Instead, we should provide some sort of clone() method
* to explicit request a copy and then move that clone
*/
for (const auto& handler: customHandlers) {
auto result = handler(Request(req, std::vector<TypedParam>(), std::vector<TypedParam>()), std::move(response));
if (result == Route::Result::Ok) return Router::Status::Match;
}
throw std::runtime_error("Fix me");
response.send(Http::Code::Not_Found, "Could not find a matching route");
return Router::Status::NotFound;
}
void
Router::addRoute(Http::Method method, std::string resource, Route::Handler handler)
{
......
......@@ -75,7 +75,7 @@ bool matchSplat(
Rest::Route
makeRoute(std::string value) {
auto noop = [](const Net::Http::Request&, Net::Http::Response) { };
auto noop = [](const Net::Http::Request&, Net::Http::Response) { return Rest::Route::Result::Ok; };
return Rest::Route(value, Net::Http::Method::Get, noop);
}
......
add_subdirectory(rapidjson)
Subproject commit ef5e74a200f5feab8714619dc1136ca9e96de004
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment