Commit 37c872c6 authored by octal's avatar octal

Added Swagger implementation

parent dfd57229
[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)
......
......@@ -37,6 +37,13 @@ public:
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();
}
......@@ -97,6 +104,7 @@ private:
.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) {
......
......@@ -57,23 +57,6 @@ private:
void setupRoutes() {
using namespace Net::Rest;
#if 0
Rest::Description apiDescription("my-api");
auto versionPath = desc.path("/v1");
versionPath
.route(desc.get("/record/:name/:value?"))
.bind(&StatsEndpoint::doRecordMetric, this)
.produces(MIME(Text, Plain))
.consumes(MIME(Star, Star))
.parameter<Rest::Type::String>("name", "The name of the metric to record", Rest::Flag::Required)
.parameter<Rest::Type::String>("value", "The value to record", Rest::Flag::Optional)
.response(Http::Code::Created, "The metric has been created")
.response(Http::Code::Not_Found, "The creation of the metric failed");
#endif
Routes::Post(router, "/record/:name/:value?", Routes::bind(&StatsEndpoint::doRecordMetric, this));
Routes::Get(router, "/value/:name", Routes::bind(&StatsEndpoint::doGetMetric, this));
Routes::Get(router, "/ready", Routes::bind(&Generic::handleReady));
......
......@@ -15,6 +15,7 @@
#include "mime.h"
#include "optional.h"
#include "router.h"
#include "iterator_adapter.h"
namespace Net {
......@@ -115,6 +116,27 @@ struct Info {
Optional<Contact> contact;
Optional<License> license;
template<typename Writer>
void serialize(Writer& writer) const {
writer.String("info");
writer.StartObject();
{
writer.String("title");
writer.String(title.c_str());
writer.String("version");
writer.String(version.c_str());
if (!description.empty()) {
writer.String("description");
writer.String(description.c_str());
}
if (!termsOfService.empty()) {
writer.String("termsOfService");
writer.String(termsOfService.c_str());
}
}
writer.EndObject();
}
};
struct InfoBuilder {
......@@ -161,10 +183,27 @@ struct Parameter {
return p;
}
template<typename Writer>
void serialize(Writer& writer) const {
writer.StartObject();
{
writer.String("name");
writer.String(name.c_str());
writer.String("description");
writer.String(description.c_str());
writer.String("required");
writer.Bool(required);
writer.String("type");
writer.String(type->typeName());
}
writer.EndObject();
}
std::string name;
std::string description;
bool required;
std::shared_ptr<DataType> type;
};
struct Response {
......@@ -172,6 +211,18 @@ struct Response {
Http::Code statusCode;
std::string description;
template<typename Writer>
void serialize(Writer& writer) const {
auto code = std::to_string(static_cast<uint32_t>(statusCode));
writer.String(code.c_str());
writer.StartObject();
{
writer.String("description");
writer.String(description.c_str());
}
writer.EndObject();
}
};
struct ResponseBuilder {
......@@ -203,6 +254,104 @@ struct Path {
std::vector<Response> responses;
Route::Handler handler;
template<typename Writer>
void serialize(Writer& writer) const {
auto serializeMimes = [&](const char* name, const std::vector<Http::Mime::MediaType>& mimes) {
if (!mimes.empty()) {
writer.String(name);
writer.StartArray();
{
for (const auto& mime: mimes) {
auto str = mime.toString();
writer.String(str.c_str());
}
}
writer.EndArray();
}
};
writer.String(methodString(method));
writer.StartObject();
{
writer.String("description");
writer.String(description.c_str());
serializeMimes("consumes", consumeMimes);
serializeMimes("produces", produceMimes);
// @Todo: create a template to serialize vectors in a generic way
if (!parameters.empty()) {
writer.String("parameters");
writer.StartArray();
{
for (const auto& parameter: parameters) {
parameter.serialize(writer);
}
}
writer.EndArray();
}
if (!responses.empty()) {
writer.String("responses");
writer.StartArray();
for (const auto& response: responses) {
response.serialize(writer);
}
writer.EndArray();
}
}
writer.EndObject();
}
};
class PathGroup {
public:
typedef std::unordered_map<std::string, std::vector<Path>> Map;
typedef Map::iterator iterator;
typedef Map::const_iterator const_iterator;
typedef std::vector<Path>::iterator group_iterator;
typedef FlatMapIteratorAdapter<Map> flat_iterator;
bool hasPath(const std::string& name, Http::Method method) const;
bool hasPath(const Path& path) const;
std::vector<Path> paths(const std::string& name) const;
Optional<Path> path(const std::string& name, Http::Method method) const;
group_iterator add(Path path);
template<typename... Args>
group_iterator emplace(Args&& ...args) {
return add(Path(std::forward<Args>(args)...));
}
const_iterator begin() const;
const_iterator end() const;
flat_iterator flatBegin() const;
flat_iterator flatEnd() const;
template<typename Writer>
void serialize(Writer& writer) const {
writer.String("paths");
writer.StartArray();
{
for (const auto& group: groups) {
writer.String(group.first.c_str());
writer.StartObject();
{
for (const auto& path: group.second) {
path.serialize(writer);
}
}
writer.EndObject();
}
}
writer.EndArray();
}
private:
Map groups;
};
struct PathBuilder {
......@@ -246,6 +395,8 @@ struct PathBuilder {
path_->handler = [=](const Rest::Request& request, Http::ResponseWriter response) {
CALL_MEMBER_FN(obj, func)(request, std::move(response));
return Route::Result::Ok;
};
#undef CALL_MEMBER_FN
......@@ -258,6 +409,8 @@ struct PathBuilder {
path_->handler = [=](const Rest::Request& request, Http::ResponseWriter response) {
func(request, std::move(response));
return Route::Result::Ok;
};
return *this;
......@@ -269,7 +422,7 @@ private:
};
struct SubPath {
SubPath(std::string prefix, std::vector<Path>* paths);
SubPath(std::string prefix, PathGroup* paths);
PathBuilder route(std::string path, Http::Method method, std::string description = "");
PathBuilder route(PathFragment fragment, std::string description = "");
......@@ -283,7 +436,7 @@ struct SubPath {
std::string prefix;
std::vector<Parameter> parameters;
std::vector<Path>* paths;
PathGroup* paths;
};
} // namespace Schema
......@@ -316,14 +469,58 @@ public:
Schema::ResponseBuilder response(Http::Code statusCode, std::string description);
std::vector<Schema::Path> paths() const { return paths_; }
Schema::PathGroup paths() const { return paths_; }
template<typename Writer>
void serialize(Writer& writer) const {
writer.StartObject();
{
info_.serialize(writer);
if (!host_.empty()) {
writer.String("host");
writer.String(host_.c_str());
}
if (!schemes_.empty()) {
writer.String("schemes");
writer.StartArray();
{
for (const auto& scheme: schemes_) {
writer.String(scheme.c_str());
}
}
writer.EndArray();
}
paths_.serialize(writer);
}
writer.EndObject();
}
private:
Schema::Info info_;
std::string host_;
std::vector<std::string> schemes_;
std::vector<Schema::Path> paths_;
Schema::PathGroup paths_;
};
class Swagger {
public:
Swagger(const Description& description)
: description_(description)
{ }
Swagger& uiPath(std::string path);
Swagger& uiDirectory(std::string dir);
Swagger& apiPath(std::string path);
void install(Rest::Router& router);
private:
Description description_;
std::string uiPath_;
std::string uiDirectory_;
std::string apiPath_;
};
} // namespace Rest
......
......@@ -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);
}
......@@ -61,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))
......@@ -129,11 +131,43 @@ private:
std::vector<Fragment> fragments_;
};
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;
std::vector<Route::Handler> customHandlers;
};
namespace Private {
class HttpHandler : public Net::Http::Handler {
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,
......@@ -141,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;
......@@ -168,27 +202,6 @@ private:
std::vector<TypedParam> splats_;
};
class Router {
public:
static Router fromDescription(const Rest::Description& desc);
std::shared_ptr<Private::HttpHandler>
handler() const {
return std::make_shared<Private::HttpHandler>(routes);
}
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);
private:
void addRoute(Http::Method method, std::string resource, Route::Handler handler);
std::unordered_map<Http::Method, std::vector<Route>> routes;
};
namespace Routes {
......@@ -231,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
......@@ -242,6 +257,8 @@ namespace Routes {
return [=](const Rest::Request& request, Http::ResponseWriter response) {
func(request, std::move(response));
return Route::Result::Ok;
};
}
......
......@@ -5,7 +5,10 @@
*/
#include "description.h"
#include "http_header.h"
#include <sstream>
#include <algorithm>
#include "rapidjson/prettywriter.h"
namespace Net {
......@@ -27,10 +30,10 @@ License::License(
{ }
Info::Info(
std::string title, std::string description, std::string version)
std::string title, std::string version, std::string description)
: title(std::move(title))
, description(std::move(description))
, version(std::move(version))
, description(std::move(description))
{ }
PathFragment::PathFragment(
......@@ -46,12 +49,77 @@ Path::Path(
, description(std::move(description))
{ }
bool
PathGroup::hasPath(const std::string& name, Http::Method method) const {
auto group = paths(name);
auto it = std::find_if(std::begin(group), std::end(group), [&](const Path& p) {
return p.method == method;
});
return it != std::end(group);
}
bool
PathGroup::hasPath(const Path& path) const {
return hasPath(path.value, path.method);
}
std::vector<Path>
PathGroup::paths(const std::string& name) const {
auto it = groups.find(name);
if (it == std::end(groups))
return std::vector<Path> { };
return it->second;
}
Optional<Path>
PathGroup::path(const std::string& name, Http::Method method) const {
auto group = paths(name);
auto it = std::find_if(std::begin(group), std::end(group), [&](const Path& p) {
return p.method == method;
});
if (it != std::end(group)) {
return Some(*it);
}
return None();
}
PathGroup::group_iterator
PathGroup::add(Path path) {
if (hasPath(path))
return PathGroup::group_iterator { };
auto &group = groups[path.value];
return group.insert(group.end(), std::move(path));
}
PathGroup::const_iterator
PathGroup::begin() const {
return groups.begin();
}
PathGroup::const_iterator
PathGroup::end() const {
return groups.end();
}
PathGroup::flat_iterator
PathGroup::flatBegin() const {
return makeFlatMapIterator(groups, begin());
}
PathGroup::flat_iterator
PathGroup::flatEnd() const {
return makeFlatMapIterator(groups, end());
}
PathBuilder::PathBuilder(Path* path)
: path_(path)
{ }
SubPath::SubPath(
std::string prefix, std::vector<Path>* paths)
std::string prefix, PathGroup* paths)
: prefix(std::move(prefix))
, paths(paths)
{ }
......@@ -62,9 +130,9 @@ SubPath::route(std::string name, Http::Method method, std::string description) {
Path path(std::move(fullPath), method, std::move(description));
std::copy(std::begin(parameters), std::end(parameters), std::back_inserter(path.parameters));
paths->push_back(std::move(path));
auto it = paths->add(std::move(path));
return PathBuilder(&paths->back());
return PathBuilder(&*it);
}
PathBuilder
......@@ -164,8 +232,8 @@ Description::path(std::string name) {
Schema::PathBuilder
Description::route(std::string name, Http::Method method, std::string description) {
paths_.emplace_back(std::move(name), method, std::move(description));
return Schema::PathBuilder(&paths_.back());
auto it = paths_.emplace(std::move(name), method, std::move(description));
return Schema::PathBuilder(&*it);
}
Schema::PathBuilder
......@@ -179,6 +247,127 @@ Description::response(Http::Code statusCode, std::string description) {
return builder;
}
Swagger&
Swagger::uiPath(std::string path) {
uiPath_ = std::move(path);
return *this;
}
Swagger&
Swagger::uiDirectory(std::string dir) {
uiDirectory_ = std::move(dir);
return *this;
}
Swagger&
Swagger::apiPath(std::string path) {
apiPath_ = std::move(path);
return *this;
}
void
Swagger::install(Rest::Router& router) {
Route::Handler uiHandler = [=](const Rest::Request& req, Http::ResponseWriter response) {
auto res = req.resource();
/*
* @Export might be useful for routing also. Make it public
*/
struct Path {
Path(const std::string& value)
: value(value)
, trailingSlashValue(value)
{
if (trailingSlashValue.back() != '/')
trailingSlashValue += '/';
}
static bool hasTrailingSlash(const Rest::Request& req) {
auto res = req.resource();
return res.back() == '/';
}
std::string stripPrefix(const Rest::Request& req) {
auto res = req.resource();
if (!res.compare(0, value.size(), value)) {
return res.substr(value.size());
}
return res;
}
bool matches(const Rest::Request& req) {
auto res = req.resource();
if (value == res)
return true;
if (res == trailingSlashValue)
return true;
return false;
}
bool isPrefix(const Rest::Request& req) {
auto res = req.resource();
return !res.compare(0, value.size(), value);
}
const std::string& withTrailingSlash() const {
return trailingSlashValue;
}
std::string join(const std::string& value) const {
std::string val;
if (value[0] == '/') val = value.substr(1);
else val = value;
return trailingSlashValue + val;
}
private:
std::string value;
std::string trailingSlashValue;
};
Path ui(uiPath_);
Path uiDir(uiDirectory_);
if (ui.matches(req)) {
if (!Path::hasTrailingSlash(req)) {
response
.headers()
.add<Http::Header::Location>(uiPath_ + '/');
response.send(Http::Code::Moved_Permanently);
} else {
auto index = uiDir.join("index.html");
Http::serveFile(response, index.c_str());
}
return Route::Result::Ok;
}
else if (ui.isPrefix(req)) {
auto file = ui.stripPrefix(req);
auto path = uiDir.join(file);
Http::serveFile(response, path.c_str());
return Route::Result::Ok;
}
else if (res == apiPath_) {
rapidjson::StringBuffer sb;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(sb);
description_.serialize(writer);
response.send(Http::Code::Ok, sb.GetString(), MIME(Application, Json));
return Route::Result::Ok;
}
return Route::Result::Failure;
};
router.addCustomHandler(uiHandler);
}
} // namespace Rest
......
......@@ -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);
......
......@@ -210,30 +210,17 @@ 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
......@@ -245,11 +232,19 @@ Router::fromDescription(const Rest::Description& 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 (const auto& path: paths) {
addRoute(path.method, std::move(path.value), std::move(path.handler));
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));
}
}
}
......@@ -273,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