From eff83bd6a60bfd45b769036839b045f7597fd00d Mon Sep 17 00:00:00 2001 From: Leo Ghafari Date: Thu, 22 Jan 2026 16:57:52 +0900 Subject: [PATCH 1/2] Setup the web-server helper --- MODULE.bazel | 8 ++-- MODULE.bazel.lock | 2 + src/BUILD.bazel | 5 ++ src/hello.cpp | 107 +++++++++++++++++++++++++++++++++++++++++-- src/hello.hpp | 68 ++++++++++++++++++++++++--- src/main.cpp | 21 +++++++-- tests/BUILD.bazel | 4 +- tests/hello_test.cpp | 8 +--- 8 files changed, 196 insertions(+), 27 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 209f3a5..d4a466a 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -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) @@ -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", ) \ No newline at end of file diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index b16c7b0..e74db2c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -69,6 +69,8 @@ "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/MODULE.bazel": "a1c8bb07b5b91d971727c635f449d05623ac9608f6fe4f5f04254ea12f08e349", + "https://bcr.bazel.build/modules/nlohmann_json/3.12.0.bcr.1/source.json": "93f82a5ae985eb935c539bfee95e04767187818189241ac956f3ccadbdb8fb02", "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", "https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", diff --git a/src/BUILD.bazel b/src/BUILD.bazel index 856112e..2a09647 100644 --- a/src/BUILD.bazel +++ b/src/BUILD.bazel @@ -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( diff --git a/src/hello.cpp b/src/hello.cpp index ae937f5..35ab2b8 100644 --- a/src/hello.cpp +++ b/src/hello.cpp @@ -2,15 +2,112 @@ #include -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 handler, + const std::string& description, + const std::map& 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& 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(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 diff --git a/src/hello.hpp b/src/hello.hpp index 1a22bde..25159eb 100644 --- a/src/hello.hpp +++ b/src/hello.hpp @@ -1,18 +1,74 @@ #pragma once +#include +#include #include -namespace rest_api_helper +#include +#include + +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 handler, + const std::string& description, + const std::map& 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& 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> routes_; }; -} // namespace rest_api_helper +} // namespace yuki::web diff --git a/src/main.cpp b/src/main.cpp index b0b7116..476d4f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,10 +1,23 @@ -#include - #include "src/hello.hpp" +#include + 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; } diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index f064a41..66ce573 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -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 = [ diff --git a/tests/hello_test.cpp b/tests/hello_test.cpp index fdcf244..f21b0cb 100644 --- a/tests/hello_test.cpp +++ b/tests/hello_test.cpp @@ -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" From 54c5c046456337a560dfd976dc2726b00d53430f Mon Sep 17 00:00:00 2001 From: Leo Ghafari Date: Thu, 22 Jan 2026 17:02:52 +0900 Subject: [PATCH 2/2] fix formatting --- src/main.cpp | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 476d4f6..cb8670f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,33 @@ -#include "src/hello.hpp" - #include +#include "src/hello.hpp" + int main() { - httplib::Server server; + 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& 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"}}); + 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);