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();
}
/*
Mathieu Stefani, 24 février 2016
An API description (reflection) mechanism that is based on Swagger
*/
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <type_traits>
#include <memory>
#include <algorithm>
#include "http_defs.h"
#include "mime.h"
#include "optional.h"
#include "router.h"
#include "iterator_adapter.h"
namespace Net {
namespace Rest {
namespace Type {
// Data Types
#define DATA_TYPE \
TYPE(Integer, std::int32_t , "integer", "int32") \
TYPE(Long , std::int64_t , "integer", "int64") \
TYPE(Float , float , "number" , "float") \
TYPE(Double , double , "number" , "double") \
TYPE(String , std::string , "string" , "") \
TYPE(Byte , char , "string" , "byte") \
TYPE(Binary , std::vector<std::uint8_t>, "string" , "binary") \
TYPE(Bool , bool , "boolean", "") \
COMPLEX_TYPE(Date , "string", "date") \
COMPLEX_TYPE(Datetime, "string", "date-time") \
COMPLEX_TYPE(Password, "string", "password") \
COMPLEX_TYPE(Array , "array", "array")
#define TYPE(rest, cpp, _, __) \
typedef cpp rest;
#define COMPLEX_TYPE(rest, _, __) \
struct rest { };
DATA_TYPE
#undef TYPE
#undef COMPLEX_TYPE
} // namespace Type
enum class Flag {
Optional, Required
};
namespace Schema {
namespace Traits {
template<typename DT> struct IsDataType : public std::false_type { };
#define TYPE(rest, _, __, ___) \
template<> struct IsDataType<Type::rest> : public std::true_type { };
#define COMPLEX_TYPE(rest, _, __) \
template<> struct IsDataType<Type::rest> : public std::true_type { };
DATA_TYPE
#undef TYPE
#undef COMPLEX_TYPE
template<typename DT> struct DataTypeInfo;
#define TYPE(rest, _, typeStr, formatStr) \
template<> struct DataTypeInfo<Type::rest> { \
static const char *typeName() { return typeStr; } \
static const char *format() { return formatStr; } \
};
#define COMPLEX_TYPE(rest, typeStr, formatStr) \
template<> struct DataTypeInfo<Type::rest> { \
static const char* typeName() { return typeStr; } \
static const char* format() { return formatStr; } \
};
DATA_TYPE
#undef TYPE
#undef COMPLEX_TYPE
template<typename DT> struct DataTypeValidation {
static bool validate(const std::string& ) {
return true;
}
};
} // namespace Traits
struct Contact {
Contact(std::string name, std::string url, std::string email);
std::string name;
std::string url;
std::string email;
};
struct License {
License(std::string name, std::string url);
std::string name;
std::string url;
};
struct Info {
Info(std::string title, std::string version, std::string description = "");
std::string title;
std::string version;
std::string description;
std::string termsOfService;
Optional<Contact> contact;
Optional<License> license;
template<typename Writer>
void serialize(Writer& writer) const {
writer.String("swagger");
writer.String("2.0");
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 {
InfoBuilder(Info* info);
InfoBuilder& termsOfService(std::string value);
InfoBuilder& contact(std::string name, std::string url, std::string email);
InfoBuilder& license(std::string name, std::string url);
private:
Info *info_;
};
struct DataType {
virtual const char* typeName() const = 0;
virtual const char* format() const = 0;
virtual bool validate(const std::string& input) const = 0;
};
template<typename T>
struct DataTypeT : public DataType {
const char* typeName() const { return Traits::DataTypeInfo<T>::typeName(); }
const char* format() const { return Traits::DataTypeInfo<T>::format(); }
bool validate(const std::string& input) const { return Traits::DataTypeValidation<T>::validate(input); }
};
template<typename T>
std::unique_ptr<DataType> makeDataType() {
static_assert(Traits::IsDataType<T>::value, "Unknown Data Type");
return std::unique_ptr<DataType>(new DataTypeT<T>());
}
struct Parameter {
Parameter(
std::string name, std::string description);
template<typename T, typename... Args>
static Parameter create(Args&& ...args) {
Parameter p(std::forward<Args>(args)...);
p.type = makeDataType<T>();
return p;
}
template<typename Writer>
void serialize(Writer& writer) const {
writer.StartObject();
{
writer.String("name");
writer.String(name.c_str());
writer.String("in");
// @Feature: support other types of parameters
writer.String("path");
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 {
Response(Http::Code statusCode, std::string description);
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 {
ResponseBuilder(Http::Code statusCode, std::string description);
operator Response() const { return response_; }
private:
Response response_;
};
struct PathFragment {
PathFragment(std::string value, Http::Method method);
std::string value;
Http::Method method;
};
struct Path {
Path(std::string path, Http::Method method, std::string description);
std::string value;
Http::Method method;
std::string description;
std::vector<Http::Mime::MediaType> produceMimes;
std::vector<Http::Mime::MediaType> consumeMimes;
std::vector<Parameter> parameters;
std::vector<Response> responses;
Route::Handler handler;
static std::string swaggerFormat(const std::string& path);
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();
}
};
std::string methodStr(methodString(method));
// So it looks like Swagger requires method to be in lowercase
std::transform(std::begin(methodStr), std::end(methodStr), std::begin(methodStr), ::tolower);
writer.String(methodStr.c_str());
writer.StartObject();
{
writer.String("description");
writer.String(description.c_str());
serializeMimes("consumes", consumeMimes);
serializeMimes("produces", produceMimes);
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.StartObject();
{
for (const auto& response: responses) {
response.serialize(writer);
}
}
writer.EndObject();
}
}
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;
enum class Format { Default, Swagger };
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, Format format = Format::Default) const {
writer.String("paths");
writer.StartObject();
{
for (const auto& group: groups) {
if (format == Format::Default) {
writer.String(group.first.c_str());
} else {
auto swaggerPath = Path::swaggerFormat(group.first);
writer.String(swaggerPath.c_str());
}
writer.StartObject();
{
for (const auto& path: group.second) {
path.serialize(writer);
}
}
writer.EndObject();
}
}
writer.EndObject();
}
private:
Map groups;
};
struct PathBuilder {
PathBuilder(Path* path);
template<typename... Mimes>
PathBuilder& produces(Mimes... mimes) {
const Http::Mime::MediaType m[] = { mimes... };
std::copy(std::begin(m), std::end(m), std::back_inserter(path_->produceMimes));
return *this;
}
template<typename... Mimes>
PathBuilder& consumes(Mimes... mimes) {
const Http::Mime::MediaType m[] = { mimes... };
std::copy(std::begin(m), std::end(m), std::back_inserter(path_->consumeMimes));
return *this;
}
template<typename T>
PathBuilder& parameter(std::string name, std::string description) {
path_->parameters.push_back(Parameter::create<T>(std::move(name), std::move(description)));
return *this;
}
PathBuilder& response(Http::Code statusCode, std::string description) {
path_->responses.push_back(Response(statusCode, std::move(description)));
return *this;
}
PathBuilder& response(Response response) {
path_->responses.push_back(std::move(response));
return *this;
}
/* @CodeDup: should re-use Routes::bind */
template<typename Result, typename Cls, typename... Args, typename Obj>
PathBuilder& bind(Result (Cls::*func)(Args...), Obj obj) {
#define CALL_MEMBER_FN(obj, pmf) ((obj)->*(pmf))
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
return *this;
}
template<typename Result, typename... Args>
PathBuilder& bind(Result (*func)(Args...)) {
path_->handler = [=](const Rest::Request& request, Http::ResponseWriter response) {
func(request, std::move(response));
return Route::Result::Ok;
};
return *this;
}
private:
Path* path_;
};
struct SubPath {
SubPath(std::string prefix, PathGroup* paths);
PathBuilder route(std::string path, Http::Method method, std::string description = "");
PathBuilder route(PathFragment fragment, std::string description = "");
SubPath path(std::string prefix);
template<typename T>
void parameter(std::string name, std::string description) {
parameters.push_back(Parameter::create<T>(std::move(name), std::move(description)));
}
std::string prefix;
std::vector<Parameter> parameters;
PathGroup* paths;
};
} // namespace Schema
class Description {
public:
Description(std::string title, std::string version, std::string description = "");
Schema::InfoBuilder info();
Description& host(std::string value);
template<typename... Scheme>
Description& schemes(Scheme... schemes) {
// @Improve: try to statically assert that every Scheme is convertible to string
const std::string s[] = { std::string(schemes)... };
std::copy(std::begin(s), std::end(s), std::back_inserter(schemes_));
return *this;
}
Schema::PathFragment get(std::string name);
Schema::PathFragment post(std::string name);
Schema::PathFragment put(std::string name);
Schema::PathFragment del(std::string name);
Schema::SubPath path(std::string name);
Schema::PathBuilder route(std::string name, Http::Method method, std::string description = "");
Schema::PathBuilder route(Schema::PathFragment fragment, std::string description = "");
Schema::ResponseBuilder response(Http::Code statusCode, std::string description);
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, Schema::PathGroup::Format::Swagger);
}
writer.EndObject();
}
private:
Schema::Info info_;
std::string host_;
std::vector<std::string> schemes_;
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
} // namespace Net
......@@ -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;
};
}
......
/* description.cc
Mathieu Stefani, 24 février 2016
Implementation of the description system
*/
#include "description.h"
#include "http_header.h"
#include <sstream>
#include <algorithm>
#include "rapidjson/prettywriter.h"
namespace Net {
namespace Rest {
namespace Schema {
Contact::Contact(
std::string name, std::string url, std::string email)
: name(std::move(name))
, url(std::move(url))
, email(std::move(email))
{ }
License::License(
std::string name, std::string url)
: name(std::move(name))
, url(std::move(url))
{ }
Info::Info(
std::string title, std::string version, std::string description)
: title(std::move(title))
, version(std::move(version))
, description(std::move(description))
{ }
PathFragment::PathFragment(
std::string value, Http::Method method)
: value(std::move(value))
, method(method)
{ }
Path::Path(
std::string value, Http::Method method, std::string description)
: value(std::move(value))
, method(method)
, description(std::move(description))
{ }
std::string
Path::swaggerFormat(const std::string& path) {
if (path.empty()) return "";
if (path[0] != '/') throw std::invalid_argument("Invalid path, should start with a '/'");
/* @CodeDup: re-use the private Fragment class of Router */
auto isPositional = [](const std::string& fragment) {
if (fragment[0] == ':')
return std::make_pair(true, fragment.substr(1));
return std::make_pair(false, std::string());
};
auto isOptional = [](const std::string& fragment) {
auto pos = fragment.find('?');
// @Validation the '?' should be the last character
return std::make_pair(pos != std::string::npos, pos);
};
std::ostringstream oss;
auto processFragment = [&](std::string fragment) {
auto optional = isOptional(fragment);
if (optional.first)
fragment.erase(optional.second, 1);
auto positional = isPositional(fragment);
if (positional.first) {
oss << '{' << positional.second << '}';
} else {
oss << fragment;
}
};
std::istringstream iss(path);
std::string fragment;
std::vector<std::string> fragments;
while (std::getline(iss, fragment, '/')) {
fragments.push_back(std::move(fragment));
}
for (size_t i = 0; i < fragments.size() - 1; ++i) {
const auto& frag = fragments[i];
processFragment(frag);
oss << '/';
}
const auto& last = fragments.back();
if (last.empty()) oss << '/';
else {
processFragment(last);
}
return oss.str();
}
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, PathGroup* paths)
: prefix(std::move(prefix))
, paths(paths)
{ }
PathBuilder
SubPath::route(std::string name, Http::Method method, std::string description) {
auto fullPath = prefix + name;
Path path(std::move(fullPath), method, std::move(description));
std::copy(std::begin(parameters), std::end(parameters), std::back_inserter(path.parameters));
auto it = paths->add(std::move(path));
return PathBuilder(&*it);
}
PathBuilder
SubPath::route(PathFragment fragment, std::string description) {
return route(std::move(fragment.value), fragment.method, std::move(description));
}
SubPath
SubPath::path(std::string prefix) {
return SubPath(this->prefix + prefix, paths);
}
Parameter::Parameter(
std::string name, std::string description)
: name(std::move(name))
, description(std::move(description))
, required(true)
{ }
Response::Response(
Http::Code statusCode, std::string description)
: statusCode(statusCode)
, description(std::move(description))
{ }
ResponseBuilder::ResponseBuilder(
Http::Code statusCode, std::string description)
: response_(statusCode, std::move(description))
{ }
InfoBuilder::InfoBuilder(Info* info)
: info_(info)
{ }
InfoBuilder&
InfoBuilder::termsOfService(std::string value) {
info_->termsOfService = std::move(value);
return *this;
}
InfoBuilder&
InfoBuilder::contact(std::string name, std::string url, std::string email) {
info_->contact = Some(Contact(std::move(name), std::move(url), std::move(email)));
return *this;
}
InfoBuilder&
InfoBuilder::license(std::string name, std::string url) {
info_->license = Some(License(std::move(name), std::move(url)));
return *this;
}
} // namespace Schema
Description::Description(
std::string title, std::string version, std::string description)
: info_(std::move(title), std::move(version), std::move(description))
{
}
Schema::InfoBuilder
Description::info() {
Schema::InfoBuilder builder(&info_);
return builder;
}
Description&
Description::host(std::string value) {
host_ = std::move(value);
return *this;
}
Schema::PathFragment
Description::get(std::string name) {
return Schema::PathFragment(std::move(name), Http::Method::Get);
}
Schema::PathFragment
Description::post(std::string name) {
return Schema::PathFragment(std::move(name), Http::Method::Post);
}
Schema::PathFragment
Description::put(std::string name) {
return Schema::PathFragment(std::move(name), Http::Method::Put);
}
Schema::PathFragment
Description::del(std::string name) {
return Schema::PathFragment(std::move(name), Http::Method::Delete);
}
Schema::SubPath
Description::path(std::string name) {
return Schema::SubPath(std::move(name), &paths_);
}
Schema::PathBuilder
Description::route(std::string name, Http::Method method, std::string description) {
auto it = paths_.emplace(std::move(name), method, std::move(description));
return Schema::PathBuilder(&*it);
}
Schema::PathBuilder
Description::route(Schema::PathFragment fragment, std::string description) {
return route(std::move(fragment.value), fragment.method, std::move(description));
}
Schema::ResponseBuilder
Description::response(Http::Code statusCode, std::string description) {
Schema::ResponseBuilder builder(statusCode, std::move(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 or merge it with the Fragment class
*/
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
} // namespace Net
......@@ -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