Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ module(
version = "0.0.1",
)

bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "rules_cc", version = "0.2.16")
bazel_dep(name = "cpp-httplib", version = "0.22.0")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1")


bazel_dep(name = "googletest", version = "1.17.0", dev_dependency = True)
bazel_dep(name = "toolchains_llvm", version = "1.6.0", dev_dependency = True)
Expand Down Expand Up @@ -36,6 +38,6 @@ register_toolchains("@llvm_toolchain//:all", dev_dependency = True)
bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
git_override(
module_name = "hedron_compile_commands",
commit = "0e990032f3c5a866e72615cf67e5ce22186dcb97",
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
remote = "https://github.com/mikael-s-persson/bazel-compile-commands-extractor.git",
commit = "f5fbd4cee671d8d908f37c83abaf70fba5928fc7",
)
2 changes: 2 additions & 0 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ strict_cc_library(
srcs = ["hello.cpp"],
hdrs = ["hello.hpp"],
visibility = ["//visibility:public"],

deps = [
"@cpp-httplib",
"@nlohmann_json//:json",
],
)

strict_cc_binary(
Expand Down
107 changes: 102 additions & 5 deletions src/hello.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,112 @@

#include <utility>

namespace rest_api_helper
namespace yuki::web
{

Greeter::Greeter(std::string name) : name_(std::move(name))
std::string http_method_to_string(HTTPMethod method)
{
switch (method)
{
case HTTPMethod::HTTP_GET:
return "GET";
case HTTPMethod::HTTP_POST:
return "POST";
case HTTPMethod::HTTP_DELETE:
return "DELETE";
case HTTPMethod::HTTP_PUT:
return "PUT";
default:
return "UNKNOWN";
}
}

RestAPI::Route::Route(httplib::Server& server,
nlohmann::json& endpoints_documentation,
const std::string& route_path) :
server_(server), endpoints_documentation_(endpoints_documentation), route_path_(route_path)
{}

std::string Greeter::say_hello() const
void RestAPI::Route::add_endpoint(
HTTPMethod method,
std::function<void(const httplib::Request&, httplib::Response&)> handler,
const std::string& description,
const std::map<std::string, std::string>& parameters_descriptions)
{
if (endpoints_documentation_.contains(http_method_to_string(method)))
{
throw std::runtime_error("Endpoint for method " + http_method_to_string(method) +
" already exists in route " + route_path_);
}

nlohmann::json endpoint;
endpoint["description"] = description;
endpoint["parameters"] = parameters_descriptions;
endpoints_documentation_[http_method_to_string(method)] = endpoint;

switch (method)
{
case HTTPMethod::HTTP_GET:
server_.Get(route_path_.c_str(), handler);
break;
case HTTPMethod::HTTP_POST:
server_.Post(route_path_.c_str(), handler);
break;
case HTTPMethod::HTTP_DELETE:
server_.Delete(route_path_.c_str(), handler);
break;
case HTTPMethod::HTTP_PUT:
server_.Put(route_path_.c_str(), handler);
break;
default:
throw std::runtime_error("Unsupported HTTP method");
}
}

RestAPI::RestAPI(httplib::Server& base_server, const std::string& base_api_route) :
server_(base_server), base_api_route_(base_api_route)
{
return "Hello, " + name_ + "!";
// Add a trailing slash if not present
if (!base_api_route_.empty() && base_api_route_.back() != '/')
{
base_api_route_ += '/';
}
}

yuki::web::RestAPI::Route&
RestAPI::add_route(const std::string& path,
const std::string& description,
const std::map<std::string, std::string>& path_parameters_descriptions)
{
if (path.empty())
{
throw std::runtime_error("Path must not be empty");
}

auto full_path = path[0] == '/' ? path : base_api_route_ + path;

if (routes_documentation_.contains(full_path))
{
throw std::runtime_error("Route already exists: " + full_path);
}

routes_documentation_[full_path]["description"] = description;
routes_documentation_[full_path]["path_parameters"] = path_parameters_descriptions;

routes_.emplace(
full_path,
std::unique_ptr<Route>(new Route(server_, routes_documentation_[full_path], full_path)));
return *routes_.at(full_path);
}

void RestAPI::add_docs_endpoint(const std::string& docs_path)
{
auto full_path = docs_path[0] == '/' ? docs_path : base_api_route_ + docs_path;

server_.Get(full_path,
[this]([[maybe_unused]] const httplib::Request& req, httplib::Response& res) {
res.set_content(routes_documentation_.dump(4), "application/json");
});
}

} // namespace rest_api_helper
} // namespace yuki::web
68 changes: 62 additions & 6 deletions src/hello.hpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
#pragma once

#include <functional>
#include <map>
#include <string>

namespace rest_api_helper
#include <httplib.h>
#include <nlohmann/json.hpp>

namespace yuki::web
{

class Greeter
enum class HTTPMethod
{
HTTP_GET,
HTTP_POST,
HTTP_DELETE,
HTTP_PUT,
};

std::string http_method_to_string(HTTPMethod method);

class RestAPI
{
public:
Greeter(std::string name);
std::string say_hello() const;
class Route
{
public:
Route(const Route&) = delete;
Route& operator=(const Route&) = delete;
Route(Route&&) = delete;
Route& operator=(Route&&) = delete;
~Route() = default;

void add_endpoint(HTTPMethod method,
std::function<void(const httplib::Request&, httplib::Response&)> handler,
const std::string& description,
const std::map<std::string, std::string>& parameters_descriptions = {});

private:
Route(httplib::Server& server,
nlohmann::json& endpoints_documentation,
const std::string& route_path);

httplib::Server& server_;
nlohmann::json& endpoints_documentation_;
std::string route_path_;

friend class yuki::web::RestAPI;
};

RestAPI(httplib::Server& base_server, const std::string& base_api_route);
RestAPI(const RestAPI&) = delete;
RestAPI& operator=(const RestAPI&) = delete;
RestAPI(RestAPI&&) = delete;
RestAPI& operator=(RestAPI&&) = delete;
~RestAPI() = default;

yuki::web::RestAPI::Route&
add_route(const std::string& path,
const std::string& description,
const std::map<std::string, std::string>& path_parameters_descriptions = {});

void add_docs_endpoint(const std::string& docs_path);

private:
std::string name_;
httplib::Server& server_;
std::string base_api_route_;
nlohmann::json routes_documentation_;

std::map<std::string, std::unique_ptr<yuki::web::RestAPI::Route>> routes_;
};

} // namespace rest_api_helper
} // namespace yuki::web
31 changes: 28 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
#include <iostream>
#include <httplib.h>

#include "src/hello.hpp"

int main()
{
rest_api_helper::Greeter greeter("World");
std::cout << greeter.say_hello() << std::endl;
httplib::Server server;
yuki::web::RestAPI api(server, "/api");

api.add_docs_endpoint("docs");

auto& r1 = api.add_route("/non_api/stuff", "Random thingy", { { "a", "desc_a" } });
r1.add_endpoint(
yuki::web::HTTPMethod::HTTP_GET,
[](const httplib::Request&, httplib::Response&) {},
"Description of the GET endpoint");
r1.add_endpoint(yuki::web::HTTPMethod::HTTP_POST,
[](const httplib::Request&, httplib::Response&) {},
"Description of the POST endpoint",
{ { "b", "desc_b" } });

auto& r2 = api.add_route("within/the/api", "More random thingy");
r2.add_endpoint(
yuki::web::HTTPMethod::HTTP_DELETE,
[](const httplib::Request&, httplib::Response&) {},
"Description of the DELETE endpoint");
r2.add_endpoint(yuki::web::HTTPMethod::HTTP_PUT,
[](const httplib::Request&, httplib::Response&) {},
"Description of the PUT endpoint",
{ { "c", "desc_c" } });

server.listen("0.0.0.0", 8080);

return 0;
}
4 changes: 2 additions & 2 deletions tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//build_utils:defs.bzl", "strict_cc_test")

cc_test(
strict_cc_test(
name = "unit_tests",
srcs = ["hello_test.cpp"],
deps = [
Expand Down
8 changes: 1 addition & 7 deletions tests/hello_test.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
#include "src/hello.hpp"

#include "gtest/gtest.h"

TEST(GreeterTest, SayHello)
{
rest_api_helper::Greeter greeter("Bazel");
EXPECT_EQ(greeter.say_hello(), "Hello, Bazel!");
}
#include "gtest/gtest.h"