From fd409a1247776ab3683633bba502ac8949932e1f Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Wed, 28 Jan 2026 14:51:47 +0000 Subject: [PATCH 01/19] Added REST web server to interface with capiocl::Engine --- CMakeLists.txt | 25 +- bruno_webapi_tests/add_consumer.bru | 23 ++ bruno_webapi_tests/add_file_dependency.bru | 23 ++ bruno_webapi_tests/add_producer.bru | 23 ++ bruno_webapi_tests/add_workflow_name.bru | 22 ++ bruno_webapi_tests/bruno.json | 9 + bruno_webapi_tests/collection.bru | 0 bruno_webapi_tests/get_commit_close_count.bru | 22 ++ .../get_commit_on_n_files_count.bru | 22 ++ bruno_webapi_tests/get_commit_rule.bru | 22 ++ bruno_webapi_tests/get_consumer.bru | 22 ++ bruno_webapi_tests/get_directory.bru | 22 ++ bruno_webapi_tests/get_excluded.bru | 22 ++ bruno_webapi_tests/get_file_dependencies.bru | 22 ++ bruno_webapi_tests/get_fire_rule.bru | 22 ++ bruno_webapi_tests/get_permanent.bru | 22 ++ bruno_webapi_tests/get_producer.bru | 22 ++ bruno_webapi_tests/get_workflow_name.bru | 16 ++ bruno_webapi_tests/remove_file.bru | 22 ++ bruno_webapi_tests/set_commit_close_count.bru | 23 ++ .../set_commit_on_n_files_count.bru | 23 ++ bruno_webapi_tests/set_commit_rule.bru | 23 ++ bruno_webapi_tests/set_directory.bru | 23 ++ bruno_webapi_tests/set_excluded.bru | 23 ++ bruno_webapi_tests/set_fire_rule.bru | 23 ++ bruno_webapi_tests/set_permanent.bru | 23 ++ capiocl.hpp | 5 + capiocl/engine.h | 4 + capiocl/webapi.h | 17 ++ src/Engine.cpp | 4 +- src/webapi.cpp | 245 ++++++++++++++++++ 31 files changed, 816 insertions(+), 3 deletions(-) create mode 100644 bruno_webapi_tests/add_consumer.bru create mode 100644 bruno_webapi_tests/add_file_dependency.bru create mode 100644 bruno_webapi_tests/add_producer.bru create mode 100644 bruno_webapi_tests/add_workflow_name.bru create mode 100644 bruno_webapi_tests/bruno.json create mode 100644 bruno_webapi_tests/collection.bru create mode 100644 bruno_webapi_tests/get_commit_close_count.bru create mode 100644 bruno_webapi_tests/get_commit_on_n_files_count.bru create mode 100644 bruno_webapi_tests/get_commit_rule.bru create mode 100644 bruno_webapi_tests/get_consumer.bru create mode 100644 bruno_webapi_tests/get_directory.bru create mode 100644 bruno_webapi_tests/get_excluded.bru create mode 100644 bruno_webapi_tests/get_file_dependencies.bru create mode 100644 bruno_webapi_tests/get_fire_rule.bru create mode 100644 bruno_webapi_tests/get_permanent.bru create mode 100644 bruno_webapi_tests/get_producer.bru create mode 100644 bruno_webapi_tests/get_workflow_name.bru create mode 100644 bruno_webapi_tests/remove_file.bru create mode 100644 bruno_webapi_tests/set_commit_close_count.bru create mode 100644 bruno_webapi_tests/set_commit_on_n_files_count.bru create mode 100644 bruno_webapi_tests/set_commit_rule.bru create mode 100644 bruno_webapi_tests/set_directory.bru create mode 100644 bruno_webapi_tests/set_excluded.bru create mode 100644 bruno_webapi_tests/set_fire_rule.bru create mode 100644 bruno_webapi_tests/set_permanent.bru create mode 100644 capiocl/webapi.h create mode 100644 src/webapi.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d11c15..f380cce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,11 +57,19 @@ FetchContent_Declare( GIT_TAG v1.4.3 ) +FetchContent_Declare( + httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.30.1 +) + + set(JSONCONS_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(JSONCONS_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(JSONCONS_BUILD_FUZZERS OFF CACHE BOOL "" FORCE) +set(HTTPLIB_USE_ZSTD_IF_AVAILABLE OFF CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(jsoncons tomlplusplus) +FetchContent_MakeAvailable(jsoncons tomlplusplus httplib) if (BUILD_PYTHON_BINDINGS) FetchContent_Declare( @@ -135,10 +143,19 @@ target_include_directories(libcapio_cl PUBLIC ${jsoncons_SOURCE_DIR}/include ${CAPIOCL_JSON_SCHEMAS_DIRECTORY} ${TOMLPLUSPLUS_SOURCE_DIR}/include + ${httplib_SOURCE_DIR} ) target_link_libraries(libcapio_cl PUBLIC) -target_link_libraries(libcapio_cl PRIVATE tomlplusplus::tomlplusplus) +target_link_libraries(libcapio_cl PRIVATE + tomlplusplus::tomlplusplus + httplib::httplib +) + +find_library(LIBANL anl) +if(LIBANL) + target_link_libraries(libcapio_cl PRIVATE ${LIBANL}) +endif () ##################################### # Install rules @@ -191,6 +208,10 @@ if (CAPIO_CL_BUILD_TESTS) GTest::gtest_main ) + if(LIBANL) + target_link_libraries(CAPIO_CL_tests PRIVATE ${LIBANL}) + endif () + target_include_directories(CAPIO_CL_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src diff --git a/bruno_webapi_tests/add_consumer.bru b/bruno_webapi_tests/add_consumer.bru new file mode 100644 index 0000000..2deebc8 --- /dev/null +++ b/bruno_webapi_tests/add_consumer.bru @@ -0,0 +1,23 @@ +meta { + name: add_consumer + type: http + seq: 3 +} + +post { + url: http://localhost:5520/consumer + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "consumer" : "sample2" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/add_file_dependency.bru b/bruno_webapi_tests/add_file_dependency.bru new file mode 100644 index 0000000..aea1c74 --- /dev/null +++ b/bruno_webapi_tests/add_file_dependency.bru @@ -0,0 +1,23 @@ +meta { + name: add_file_dependency + type: http + seq: 5 +} + +post { + url: http://localhost:5520/dependency + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "dependency" : "myFile.dat" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/add_producer.bru b/bruno_webapi_tests/add_producer.bru new file mode 100644 index 0000000..9a5db42 --- /dev/null +++ b/bruno_webapi_tests/add_producer.bru @@ -0,0 +1,23 @@ +meta { + name: add_producer + type: http + seq: 4 +} + +post { + url: http://localhost:5520/producer + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "producer" : "sample1" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/add_workflow_name.bru b/bruno_webapi_tests/add_workflow_name.bru new file mode 100644 index 0000000..caad638 --- /dev/null +++ b/bruno_webapi_tests/add_workflow_name.bru @@ -0,0 +1,22 @@ +meta { + name: add_workflow_name + type: http + seq: 1 +} + +post { + url: http://localhost:5520/workflow + body: json + auth: inherit +} + +body:json { + { + "name": "workflow_demo" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/bruno.json b/bruno_webapi_tests/bruno.json new file mode 100644 index 0000000..04d4d50 --- /dev/null +++ b/bruno_webapi_tests/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "bruno_webapi_tests", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno_webapi_tests/collection.bru b/bruno_webapi_tests/collection.bru new file mode 100644 index 0000000..e69de29 diff --git a/bruno_webapi_tests/get_commit_close_count.bru b/bruno_webapi_tests/get_commit_close_count.bru new file mode 100644 index 0000000..8cf6d38 --- /dev/null +++ b/bruno_webapi_tests/get_commit_close_count.bru @@ -0,0 +1,22 @@ +meta { + name: get_commit_close_count + type: http + seq: 19 +} + +get { + url: http://localhost:5520/commit/close-count + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_commit_on_n_files_count.bru b/bruno_webapi_tests/get_commit_on_n_files_count.bru new file mode 100644 index 0000000..fcd8e32 --- /dev/null +++ b/bruno_webapi_tests/get_commit_on_n_files_count.bru @@ -0,0 +1,22 @@ +meta { + name: get_commit_on_n_files_count + type: http + seq: 18 +} + +get { + url: http://localhost:5520/commit/file-count + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_commit_rule.bru b/bruno_webapi_tests/get_commit_rule.bru new file mode 100644 index 0000000..17ebf94 --- /dev/null +++ b/bruno_webapi_tests/get_commit_rule.bru @@ -0,0 +1,22 @@ +meta { + name: get_commit_rule + type: http + seq: 17 +} + +get { + url: http://localhost:5520/commit + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_consumer.bru b/bruno_webapi_tests/get_consumer.bru new file mode 100644 index 0000000..4a1fed0 --- /dev/null +++ b/bruno_webapi_tests/get_consumer.bru @@ -0,0 +1,22 @@ +meta { + name: get_consumer + type: http + seq: 15 +} + +get { + url: http://localhost:5520/consumer + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_directory.bru b/bruno_webapi_tests/get_directory.bru new file mode 100644 index 0000000..213cf03 --- /dev/null +++ b/bruno_webapi_tests/get_directory.bru @@ -0,0 +1,22 @@ +meta { + name: get_directory + type: http + seq: 23 +} + +get { + url: http://localhost:5520/directory + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_excluded.bru b/bruno_webapi_tests/get_excluded.bru new file mode 100644 index 0000000..d347f10 --- /dev/null +++ b/bruno_webapi_tests/get_excluded.bru @@ -0,0 +1,22 @@ +meta { + name: get_excluded + type: http + seq: 22 +} + +get { + url: http://localhost:5520/exclude + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_file_dependencies.bru b/bruno_webapi_tests/get_file_dependencies.bru new file mode 100644 index 0000000..b641eb2 --- /dev/null +++ b/bruno_webapi_tests/get_file_dependencies.bru @@ -0,0 +1,22 @@ +meta { + name: get_file_dependencies + type: http + seq: 16 +} + +get { + url: http://localhost:5520/dependency + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_fire_rule.bru b/bruno_webapi_tests/get_fire_rule.bru new file mode 100644 index 0000000..7bd3a5a --- /dev/null +++ b/bruno_webapi_tests/get_fire_rule.bru @@ -0,0 +1,22 @@ +meta { + name: get_fire_rule + type: http + seq: 20 +} + +get { + url: http://localhost:5520/fire + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_permanent.bru b/bruno_webapi_tests/get_permanent.bru new file mode 100644 index 0000000..67eabde --- /dev/null +++ b/bruno_webapi_tests/get_permanent.bru @@ -0,0 +1,22 @@ +meta { + name: get_permanent + type: http + seq: 21 +} + +get { + url: http://localhost:5520/permanent + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_producer.bru b/bruno_webapi_tests/get_producer.bru new file mode 100644 index 0000000..1bdb053 --- /dev/null +++ b/bruno_webapi_tests/get_producer.bru @@ -0,0 +1,22 @@ +meta { + name: get_producer + type: http + seq: 14 +} + +get { + url: http://localhost:5520/producer + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/get_workflow_name.bru b/bruno_webapi_tests/get_workflow_name.bru new file mode 100644 index 0000000..7855d58 --- /dev/null +++ b/bruno_webapi_tests/get_workflow_name.bru @@ -0,0 +1,16 @@ +meta { + name: get_workflow_name + type: http + seq: 13 +} + +get { + url: http://localhost:5520/workflow + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/remove_file.bru b/bruno_webapi_tests/remove_file.bru new file mode 100644 index 0000000..c92023c --- /dev/null +++ b/bruno_webapi_tests/remove_file.bru @@ -0,0 +1,22 @@ +meta { + name: remove_file + type: http + seq: 24 +} + +delete { + url: http://localhost:5520 + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_commit_close_count.bru b/bruno_webapi_tests/set_commit_close_count.bru new file mode 100644 index 0000000..5b61861 --- /dev/null +++ b/bruno_webapi_tests/set_commit_close_count.bru @@ -0,0 +1,23 @@ +meta { + name: set_commit_close_count + type: http + seq: 8 +} + +post { + url: http://localhost:5520/commit/close-count + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "count" : 123 + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_commit_on_n_files_count.bru b/bruno_webapi_tests/set_commit_on_n_files_count.bru new file mode 100644 index 0000000..5c6531e --- /dev/null +++ b/bruno_webapi_tests/set_commit_on_n_files_count.bru @@ -0,0 +1,23 @@ +meta { + name: set_commit_on_n_files_count + type: http + seq: 7 +} + +post { + url: http://localhost:5520/commit/file-count + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "count" : 10 + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_commit_rule.bru b/bruno_webapi_tests/set_commit_rule.bru new file mode 100644 index 0000000..c95674c --- /dev/null +++ b/bruno_webapi_tests/set_commit_rule.bru @@ -0,0 +1,23 @@ +meta { + name: set_commit_rule + type: http + seq: 6 +} + +post { + url: http://localhost:5520/commit + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "commit" : "on_close" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_directory.bru b/bruno_webapi_tests/set_directory.bru new file mode 100644 index 0000000..ecc2353 --- /dev/null +++ b/bruno_webapi_tests/set_directory.bru @@ -0,0 +1,23 @@ +meta { + name: set_directory + type: http + seq: 12 +} + +post { + url: http://localhost:5520/directory + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "directory" : true + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_excluded.bru b/bruno_webapi_tests/set_excluded.bru new file mode 100644 index 0000000..68af243 --- /dev/null +++ b/bruno_webapi_tests/set_excluded.bru @@ -0,0 +1,23 @@ +meta { + name: set_excluded + type: http + seq: 11 +} + +post { + url: http://localhost:5520/exclude + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "excluded" : true + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_fire_rule.bru b/bruno_webapi_tests/set_fire_rule.bru new file mode 100644 index 0000000..69e9022 --- /dev/null +++ b/bruno_webapi_tests/set_fire_rule.bru @@ -0,0 +1,23 @@ +meta { + name: set_fire_rule + type: http + seq: 9 +} + +post { + url: http://localhost:5520/fire + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "fire" : "no_update" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/bruno_webapi_tests/set_permanent.bru b/bruno_webapi_tests/set_permanent.bru new file mode 100644 index 0000000..03ac5b1 --- /dev/null +++ b/bruno_webapi_tests/set_permanent.bru @@ -0,0 +1,23 @@ +meta { + name: set_permanent + type: http + seq: 10 +} + +post { + url: http://localhost:5520/permanent + body: json + auth: inherit +} + +body:json { + { + "path" : "/tmp/test.txt", + "permanent" : true + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/capiocl.hpp b/capiocl.hpp index aeac085..938cad3 100644 --- a/capiocl.hpp +++ b/capiocl.hpp @@ -91,6 +91,11 @@ class CapioClConfiguration; class CapioClConfigurationException; struct defaults; } // namespace configuration + +namespace webapi { +class CapioClWebApiServer; +} + } // namespace capiocl #endif // CAPIO_CL_CAPIOCL_HPP \ No newline at end of file diff --git a/capiocl/engine.h b/capiocl/engine.h index b336969..4bf83c6 100644 --- a/capiocl/engine.h +++ b/capiocl/engine.h @@ -5,6 +5,7 @@ #include "capiocl.hpp" #include "capiocl/monitor.h" #include "capiocl/serializer.h" +#include "capiocl/webapi.h" /// @brief Namespace containing the CAPIO-CL Engine namespace capiocl::engine { @@ -39,6 +40,9 @@ class Engine final { /// @brief Name of the current workflow name std::string workflow_name; + /// @brief CAPIO-CL APIs Web Server + webapi::CapioClWebApiServer webapi_server; + // LCOV_EXCL_START /// @brief Internal CAPIO-CL Engine storage entity. Each CapioCLEntry is an entry for a given /// file handled by CAPIO-CL diff --git a/capiocl/webapi.h b/capiocl/webapi.h new file mode 100644 index 0000000..0d278bb --- /dev/null +++ b/capiocl/webapi.h @@ -0,0 +1,17 @@ +#ifndef CAPIO_CL_WEBAPI_H +#define CAPIO_CL_WEBAPI_H +#include + +#include "capiocl.hpp" + +class capiocl::webapi::CapioClWebApiServer { + + std::thread _webApiThread; + + public: + CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, + int web_server_port); + ~CapioClWebApiServer(); +}; + +#endif // CAPIO_CL_WEBAPI_H diff --git a/src/Engine.cpp b/src/Engine.cpp index 213ed1d..66e1575 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -5,6 +5,7 @@ #include "capiocl.hpp" #include "capiocl/configuration.h" #include "capiocl/engine.h" +#include "capiocl/monitor.h" #include "capiocl/printer.h" void capiocl::engine::Engine::print() const { @@ -126,7 +127,8 @@ void capiocl::engine::Engine::print() const { printer::print(printer::CLI_LEVEL_JSON, ""); } -capiocl::engine::Engine::Engine(const bool use_default_settings) { +capiocl::engine::Engine::Engine(const bool use_default_settings) + : webapi_server(this, "127.0.0.1", 5520) { node_name = std::string(1024, '\0'); gethostname(node_name.data(), node_name.size()); node_name.resize(std::strlen(node_name.c_str())); diff --git a/src/webapi.cpp b/src/webapi.cpp new file mode 100644 index 0000000..203ce85 --- /dev/null +++ b/src/webapi.cpp @@ -0,0 +1,245 @@ +#include "httplib.h" +#include "jsoncons/json.hpp" + +#include "capiocl/engine.h" +#include "capiocl/printer.h" +#include "capiocl/webapi.h" + +#define OK_RESPONSE(res) \ + res.status = 200; \ + res.set_content("OK", "text/plain"); + +#define ERROR_RESPONSE(res, e) \ + res.status = 400; \ + res.set_content("{\"error\" : \"" + std::string("Invalid request BODY data: ") + e.what() + \ + "\"}", \ + "text/plain"); + +#define JSON_RESPONSE(res, json_body) \ + res.status = 200; \ + res.set_content(json_body.as_string(), "application/json"); + +#define PROCESS_POST_REQUEST(req, res, code) \ + try { \ + jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); \ + code; \ + OK_RESPONSE(res); \ + } catch (const std::exception &e) { \ + ERROR_RESPONSE(res, e); \ + } + +#define PROCESS_GET_REQUEST(req, res, code) \ + try { \ + jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); \ + jsoncons::json reply; \ + code; \ + JSON_RESPONSE(res, reply); \ + } catch (const std::exception &e) { \ + ERROR_RESPONSE(res, e); \ + } + +void server(const std::string &address, int port, capiocl::engine::Engine *engine); + +capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, + const std::string &web_server_address, + const int web_server_port) + : _webApiThread(std::thread(server, web_server_address, web_server_port, engine)) { + + printer::print(printer::CLI_LEVEL_INFO, "API server started on " + web_server_address + ":" + + std::to_string(web_server_port)); +} + +capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { + pthread_cancel(_webApiThread.native_handle()); + _webApiThread.join(); + printer::print(printer::CLI_LEVEL_INFO, "API server stopped"); +} + +void server(const std::string &address, const int port, capiocl::engine::Engine *engine) { + + httplib::Server _server; + + _server.Post("/new", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + engine->newFile(path); + }) + }); + + _server.Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto producer = request_body["producer"].as(); + engine->addProducer(path, producer); + }); + }); + + _server.Get("/producer", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["producers"] = engine->getProducers(path); + }); + }); + + _server.Post("/consumer", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto consumer = request_body["consumer"].as(); + engine->addConsumer(path, consumer); + }); + }); + + _server.Get("/consumer", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["consumers"] = engine->getConsumers(path); + }); + }); + + _server.Post("/dependency", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto dependency = std::filesystem::path(request_body["dependency"].as()); + engine->addFileDependency(path, dependency); + }); + }); + + _server.Get("/dependency", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + std::vector deps; + for (const auto file : engine->getCommitOnFileDependencies(path)) { + deps.emplace_back(file.string()); + } + reply["dependencies"] = deps; + }); + }); + + _server.Post("/commit", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto commit_rule = request_body["commit"].as(); + engine->setCommitRule(path, commit_rule); + }); + }); + + _server.Get("/commit", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["commit"] = engine->getCommitRule(path); + }); + }); + + _server.Post("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto count = request_body["count"].as(); + engine->setDirectoryFileCount(path, count); + }); + }); + + _server.Get("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["count"] = engine->getDirectoryFileCount(path); + }); + }); + + _server.Post("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto count = request_body["count"].as(); + engine->setCommitedCloseNumber(path, count); + }); + }); + + _server.Get("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["count"] = engine->getCommitCloseCount(path); + }); + }); + + _server.Post("/fire", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + auto fire_rule = request_body["fire"].as(); + engine->setFireRule(path, fire_rule); + }); + }); + + _server.Get("/fire", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["fire"] = engine->getFireRule(path); + }); + }); + + _server.Post("/permanent", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + const auto permanent = request_body["permanent"].as(); + engine->setPermanent(path, permanent); + }); + }); + + _server.Get("/permanent", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["permanent"] = engine->isPermanent(path); + }); + }); + + _server.Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + const auto excluded = request_body["excluded"].as(); + engine->setExclude(path, excluded); + }); + }); + + _server.Get("/exclude", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["exclude"] = engine->isExcluded(path); + }); + }); + + _server.Post("/directory", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + if (request_body["directory"].as()) { + engine->setDirectory(path); + } else { + engine->setFile(path); + } + }); + }); + + _server.Get("/directory", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto path = request_body["path"].as(); + reply["directory"] = engine->isDirectory(path); + }); + }); + + _server.Post("/workflow", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto workflow_name = request_body["name"].as(); + engine->setWorkflowName(workflow_name); + }); + }); + + _server.Get("/workflow", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { reply["name"] = engine->getWorkflowName(); }); + }); + + _server.Delete("/", [&](const httplib::Request &req, httplib::Response &res) { + PROCESS_POST_REQUEST(req, res, { + const auto path = request_body["path"].as(); + engine->remove(path); + }); + }); + + _server.listen(address, port); +} \ No newline at end of file From 6739a956992fcf7d667c20a7ee924958ff13febc Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Wed, 28 Jan 2026 15:35:42 +0000 Subject: [PATCH 02/19] termination fixes --- capiocl/webapi.h | 2 ++ src/webapi.cpp | 68 ++++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/capiocl/webapi.h b/capiocl/webapi.h index 0d278bb..aea9b17 100644 --- a/capiocl/webapi.h +++ b/capiocl/webapi.h @@ -2,11 +2,13 @@ #define CAPIO_CL_WEBAPI_H #include +#include "httplib.h" #include "capiocl.hpp" class capiocl::webapi::CapioClWebApiServer { std::thread _webApiThread; + httplib::Server _webApiServer; public: CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, diff --git a/src/webapi.cpp b/src/webapi.cpp index 203ce85..3889998 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -38,35 +38,35 @@ ERROR_RESPONSE(res, e); \ } -void server(const std::string &address, int port, capiocl::engine::Engine *engine); +void server(httplib::Server *_server, const std::string &address, int port, + capiocl::engine::Engine *engine); capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, const int web_server_port) - : _webApiThread(std::thread(server, web_server_address, web_server_port, engine)) { - - printer::print(printer::CLI_LEVEL_INFO, "API server started on " + web_server_address + ":" + - std::to_string(web_server_port)); -} + : _webApiThread( + std::thread(server, &_webApiServer, web_server_address, web_server_port, engine)) {} capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { - pthread_cancel(_webApiThread.native_handle()); + _webApiServer.stop(); _webApiThread.join(); printer::print(printer::CLI_LEVEL_INFO, "API server stopped"); } -void server(const std::string &address, const int port, capiocl::engine::Engine *engine) { +void server(httplib::Server *_server, const std::string &address, const int port, + capiocl::engine::Engine *engine) { - httplib::Server _server; + capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, + "Starting API server @ " + address + ":" + std::to_string(port)); - _server.Post("/new", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/new", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); engine->newFile(path); }) }); - _server.Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto producer = request_body["producer"].as(); @@ -74,14 +74,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/producer", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/producer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["producers"] = engine->getProducers(path); }); }); - _server.Post("/consumer", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/consumer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto consumer = request_body["consumer"].as(); @@ -89,14 +89,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/consumer", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/consumer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["consumers"] = engine->getConsumers(path); }); }); - _server.Post("/dependency", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/dependency", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto dependency = std::filesystem::path(request_body["dependency"].as()); @@ -104,7 +104,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/dependency", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/dependency", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); std::vector deps; @@ -115,7 +115,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Post("/commit", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/commit", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto commit_rule = request_body["commit"].as(); @@ -123,14 +123,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/commit", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/commit", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["commit"] = engine->getCommitRule(path); }); }); - _server.Post("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto count = request_body["count"].as(); @@ -138,14 +138,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["count"] = engine->getDirectoryFileCount(path); }); }); - _server.Post("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto count = request_body["count"].as(); @@ -153,14 +153,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["count"] = engine->getCommitCloseCount(path); }); }); - _server.Post("/fire", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/fire", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto fire_rule = request_body["fire"].as(); @@ -168,14 +168,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/fire", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/fire", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["fire"] = engine->getFireRule(path); }); }); - _server.Post("/permanent", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/permanent", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); const auto permanent = request_body["permanent"].as(); @@ -183,14 +183,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/permanent", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/permanent", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["permanent"] = engine->isPermanent(path); }); }); - _server.Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); const auto excluded = request_body["excluded"].as(); @@ -198,14 +198,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/exclude", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/exclude", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["exclude"] = engine->isExcluded(path); }); }); - _server.Post("/directory", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/directory", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); if (request_body["directory"].as()) { @@ -216,30 +216,30 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); }); - _server.Get("/directory", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/directory", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["directory"] = engine->isDirectory(path); }); }); - _server.Post("/workflow", [&](const httplib::Request &req, httplib::Response &res) { + _server->Post("/workflow", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto workflow_name = request_body["name"].as(); engine->setWorkflowName(workflow_name); }); }); - _server.Get("/workflow", [&](const httplib::Request &req, httplib::Response &res) { + _server->Get("/workflow", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { reply["name"] = engine->getWorkflowName(); }); }); - _server.Delete("/", [&](const httplib::Request &req, httplib::Response &res) { + _server->Delete("/", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); engine->remove(path); }); }); - _server.listen(address, port); + _server->listen(address, port); } \ No newline at end of file From 1a5eb9fa4f17939a2280cb62fa73a2a8f0c9be26 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Wed, 28 Jan 2026 16:27:34 +0000 Subject: [PATCH 03/19] Fixed a bug in termination by downgrading httplib --- CMakeLists.txt | 2 +- capiocl/webapi.h | 5 ++- src/webapi.cpp | 101 +++++++++++++++++++++++++++++++---------------- 3 files changed, 71 insertions(+), 37 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f380cce..8c33399 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,7 @@ FetchContent_Declare( FetchContent_Declare( httplib GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git - GIT_TAG v0.30.1 + GIT_TAG v0.29.0 ) diff --git a/capiocl/webapi.h b/capiocl/webapi.h index aea9b17..039d12f 100644 --- a/capiocl/webapi.h +++ b/capiocl/webapi.h @@ -2,13 +2,14 @@ #define CAPIO_CL_WEBAPI_H #include -#include "httplib.h" #include "capiocl.hpp" class capiocl::webapi::CapioClWebApiServer { std::thread _webApiThread; - httplib::Server _webApiServer; + int _port; + + char _secretKey[256]; public: CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, diff --git a/src/webapi.cpp b/src/webapi.cpp index 3889998..d5c4313 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -38,35 +38,52 @@ ERROR_RESPONSE(res, e); \ } -void server(httplib::Server *_server, const std::string &address, int port, - capiocl::engine::Engine *engine); +void server(const std::string &address, int port, capiocl::engine::Engine *engine, + char secret_term_key[256]); capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, const int web_server_port) - : _webApiThread( - std::thread(server, &_webApiServer, web_server_address, web_server_port, engine)) {} + : _webApiThread(std::thread(server, web_server_address, web_server_port, engine, _secretKey)), + _port(web_server_port) { + FILE *f = fopen("/dev/urandom", "rb"); + if (!f) { + perror("fopen"); + exit(1); + } + + if (fread(_secretKey, 1, 256, f) != 256) { + perror("fread"); + fclose(f); + exit(1); + } + + fclose(f); + _webApiThread.detach(); +} capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { - _webApiServer.stop(); - _webApiThread.join(); - printer::print(printer::CLI_LEVEL_INFO, "API server stopped"); + + httplib::Client client("http://127.0.0.1:" + std::to_string(_port)); + client.Get("/terminate"); } -void server(httplib::Server *_server, const std::string &address, const int port, - capiocl::engine::Engine *engine) { +void server(const std::string &address, const int port, capiocl::engine::Engine *engine, + char secret_term_key[256]) { capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "Starting API server @ " + address + ":" + std::to_string(port)); - _server->Post("/new", [&](const httplib::Request &req, httplib::Response &res) { + httplib::Server _server; + + _server.Post("/new", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); engine->newFile(path); }) }); - _server->Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto producer = request_body["producer"].as(); @@ -74,14 +91,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/producer", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/producer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["producers"] = engine->getProducers(path); }); }); - _server->Post("/consumer", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/consumer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto consumer = request_body["consumer"].as(); @@ -89,14 +106,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/consumer", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/consumer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["consumers"] = engine->getConsumers(path); }); }); - _server->Post("/dependency", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/dependency", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto dependency = std::filesystem::path(request_body["dependency"].as()); @@ -104,7 +121,7 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/dependency", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/dependency", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); std::vector deps; @@ -115,7 +132,7 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Post("/commit", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/commit", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto commit_rule = request_body["commit"].as(); @@ -123,14 +140,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/commit", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/commit", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["commit"] = engine->getCommitRule(path); }); }); - _server->Post("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto count = request_body["count"].as(); @@ -138,14 +155,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["count"] = engine->getDirectoryFileCount(path); }); }); - _server->Post("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto count = request_body["count"].as(); @@ -153,14 +170,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["count"] = engine->getCommitCloseCount(path); }); }); - _server->Post("/fire", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/fire", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); auto fire_rule = request_body["fire"].as(); @@ -168,14 +185,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/fire", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/fire", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["fire"] = engine->getFireRule(path); }); }); - _server->Post("/permanent", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/permanent", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); const auto permanent = request_body["permanent"].as(); @@ -183,14 +200,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/permanent", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/permanent", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["permanent"] = engine->isPermanent(path); }); }); - _server->Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); const auto excluded = request_body["excluded"].as(); @@ -198,14 +215,14 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/exclude", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/exclude", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["exclude"] = engine->isExcluded(path); }); }); - _server->Post("/directory", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/directory", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); if (request_body["directory"].as()) { @@ -216,30 +233,46 @@ void server(httplib::Server *_server, const std::string &address, const int port }); }); - _server->Get("/directory", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/directory", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { const auto path = request_body["path"].as(); reply["directory"] = engine->isDirectory(path); }); }); - _server->Post("/workflow", [&](const httplib::Request &req, httplib::Response &res) { + _server.Post("/workflow", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto workflow_name = request_body["name"].as(); engine->setWorkflowName(workflow_name); }); }); - _server->Get("/workflow", [&](const httplib::Request &req, httplib::Response &res) { + _server.Get("/workflow", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { reply["name"] = engine->getWorkflowName(); }); }); - _server->Delete("/", [&](const httplib::Request &req, httplib::Response &res) { + _server.Delete("/", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); engine->remove(path); }); }); - _server->listen(address, port); + _server.Get("/terminate", [&]([[maybe_unused]] const httplib::Request &req, + [[maybe_unused]] httplib::Response &res) { + PROCESS_GET_REQUEST(req, res, { + const auto req_key = request_body["key"].as(); + bool match = true; + for (size_t i = 0; i < req_key.size() && match; i++) { + match = secret_term_key[i] == req_key[i]; + } + + if (match) { + capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "API server stopped"); + _server.stop(); + } + }) + }); + + _server.listen(address, port); } \ No newline at end of file From 3cf1d2ebe4ed381e33c4692c78f70951080c52c8 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Wed, 28 Jan 2026 16:36:46 +0000 Subject: [PATCH 04/19] Documentation --- capiocl/webapi.h | 11 ++++++++++- src/webapi.cpp | 50 ++++++++++++++++++------------------------------ 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/capiocl/webapi.h b/capiocl/webapi.h index 039d12f..f3e5a9d 100644 --- a/capiocl/webapi.h +++ b/capiocl/webapi.h @@ -4,16 +4,25 @@ #include "capiocl.hpp" +/// @brief Class that exposes a REST Web Server to interact with the current configuration class capiocl::webapi::CapioClWebApiServer { + /// @brief asynchronous running webserver thread std::thread _webApiThread; + + /// @brief port on which the current server runs int _port; - char _secretKey[256]; + /// @brief secret key initialized at random. used when requesting shutdown so that the + /// termination can only occur when called from the main CAPIO-CL thread + char _secretKey[256]{}; public: + /// @brief default constructor. CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, int web_server_port); + + /// @brief Default Destructor ~CapioClWebApiServer(); }; diff --git a/src/webapi.cpp b/src/webapi.cpp index d5c4313..92b9c09 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -38,38 +38,9 @@ ERROR_RESPONSE(res, e); \ } -void server(const std::string &address, int port, capiocl::engine::Engine *engine, - char secret_term_key[256]); - -capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, - const std::string &web_server_address, - const int web_server_port) - : _webApiThread(std::thread(server, web_server_address, web_server_port, engine, _secretKey)), - _port(web_server_port) { - FILE *f = fopen("/dev/urandom", "rb"); - if (!f) { - perror("fopen"); - exit(1); - } - - if (fread(_secretKey, 1, 256, f) != 256) { - perror("fread"); - fclose(f); - exit(1); - } - - fclose(f); - _webApiThread.detach(); -} - -capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { - - httplib::Client client("http://127.0.0.1:" + std::to_string(_port)); - client.Get("/terminate"); -} - +/// @brief Main WebServer thread function void server(const std::string &address, const int port, capiocl::engine::Engine *engine, - char secret_term_key[256]) { + const char secret_term_key[256]) { capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "Starting API server @ " + address + ":" + std::to_string(port)); @@ -275,4 +246,21 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.listen(address, port); +} + +capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, + const std::string &web_server_address, + const int web_server_port) + : _webApiThread(std::thread(server, web_server_address, web_server_port, engine, _secretKey)), + _port(web_server_port) { + FILE *f = fopen("/dev/urandom", "rb"); + fread(_secretKey, 1, 256, f); + fclose(f); + _webApiThread.detach(); +} + +capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { + + httplib::Client client("http://127.0.0.1:" + std::to_string(_port)); + client.Get("/terminate"); } \ No newline at end of file From ef3a67642eec25e6b4a2665e3a82523bb602add6 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Thu, 29 Jan 2026 18:31:42 +0000 Subject: [PATCH 05/19] Added decorator --- capiocl.hpp | 5 +- py_capio_cl/decorators.py | 121 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- src/webapi.cpp | 8 +-- 4 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 py_capio_cl/decorators.py diff --git a/capiocl.hpp b/capiocl.hpp index 938cad3..078366b 100644 --- a/capiocl.hpp +++ b/capiocl.hpp @@ -28,7 +28,7 @@ inline std::string sanitize(const std::string &input) { } else if (input == UPDATE) { return UPDATE; } else { - throw std::invalid_argument("Input commit rule is not a vlid CAPIO-CL rule"); + throw std::invalid_argument("Input fire rule: " + input + " is not a valid CAPIO-CL rule"); } } } // namespace fireRules @@ -59,7 +59,8 @@ inline std::string sanitize(const std::string &input) { } else if (input == ON_TERMINATION) { return ON_TERMINATION; } else { - throw std::invalid_argument("Input commit rule is not a vlid CAPIO-CL rule"); + throw std::invalid_argument("Input commit rule: " + input + + " is not a valid CAPIO-CL rule"); } } } // namespace commitRules diff --git a/py_capio_cl/decorators.py b/py_capio_cl/decorators.py new file mode 100644 index 0000000..04af942 --- /dev/null +++ b/py_capio_cl/decorators.py @@ -0,0 +1,121 @@ +from functools import wraps + +import requests + + +def CapioCLRule(path: str, + committed: str | None = None, + fire: str | None = None, + close_count: int | None = None, + directory_n_file_expected: int | None = None, + is_directory: bool | None = None, + is_permanent: bool | None = None, + is_excluded: bool | None = None, + producers: list[str] | None = None, + consumers: list[str] | None = None, + file_dependencies: list[str] | None = None + ): + if not path: + raise RuntimeError("ERROR: cannot specify a CAPIO-CL rule without setting a path!") + + def _perform_request(endpoint, payload=None): + response = requests.post(endpoint, json=payload, headers={"content-type": "application/json"}) + json = response.json() + if "OK" not in json["status"]: + print(f"ERR: {json["what"]}") + + if committed: + _perform_request( + endpoint="http://localhost:5520/commit", + payload={ + "path": path, + "commit": committed + }) + + if fire: + _perform_request( + endpoint="http://localhost:5520/fire", + payload={ + "path": path, + "fire": fire + }) + + if close_count: + _perform_request( + endpoint="http://localhost:5520/commit/close-count", + payload={ + "path": path, + "count": close_count + }) + + if directory_n_file_expected: + _perform_request( + endpoint="http://localhost:5520/commit/file-count", + payload={ + "path": path, + "count": directory_n_file_expected + }) + + if is_directory is not None: + _perform_request( + endpoint="http://localhost:5520/directory", + payload={ + "path": path, + "directory": is_directory + } + ) + + if is_permanent is not None: + _perform_request( + endpoint="http://localhost:5520/permanent", + payload={ + "path": path, + "permanent": is_permanent + } + ) + if is_excluded is not None: + _perform_request( + endpoint="http://localhost:5520/exclude", + payload={ + "path": path, + "excluded": is_excluded + } + ) + + if producers: + for producer in producers: + _perform_request( + endpoint="http://localhost:5520/producer", + payload={ + "path": path, + "producer": producer + } + ) + + if consumers: + for consumer in consumers: + _perform_request( + endpoint="http://localhost:5520/consumer", + payload={ + "path": path, + "consumer": consumer + } + ) + + if file_dependencies: + for dependency in file_dependencies: + _perform_request( + endpoint="http://localhost:5520/dependency", + payload={ + "path": path, + "dependency": dependency + } + ) + + def _capiocl_rule(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + + return _capiocl_rule diff --git a/pyproject.toml b/pyproject.toml index f7e3aee..77b13b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,8 @@ requires = [ "scikit-build-core[pyproject]==0.11.6", "pybind11==3.0.1", "setuptools==80.9.0", - "wheel==0.45.1" + "wheel==0.45.1", + "requests==2.32.5" ] build-backend = "scikit_build_core.build" diff --git a/src/webapi.cpp b/src/webapi.cpp index 92b9c09..e548e3c 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -7,13 +7,13 @@ #define OK_RESPONSE(res) \ res.status = 200; \ - res.set_content("OK", "text/plain"); + res.set_content("{\"status\" : \"OK\"}", "application/json"); #define ERROR_RESPONSE(res, e) \ res.status = 400; \ - res.set_content("{\"error\" : \"" + std::string("Invalid request BODY data: ") + e.what() + \ - "\"}", \ - "text/plain"); + res.set_content("{\"status\" : \"error\", \"what\" : \"" + \ + std::string("Invalid request BODY data: ") + e.what() + "\"}", \ + "application/json"); #define JSON_RESPONSE(res, json_body) \ res.status = 200; \ From a26178a4e32856d1213694d8f668fc7a29a1ca69 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 11:13:54 +0000 Subject: [PATCH 06/19] Began adding unit test to API webserver --- .github/workflows/ci-test.yml | 6 +- .github/workflows/python-bindings.yml | 4 +- CMakeLists.txt | 4 + pyproject.toml | 6 +- tests/cpp/main.cpp | 3 +- tests/cpp/test_apis.hpp | 123 ++++++++++++++++++++++++++ 6 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 tests/cpp/test_apis.hpp diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index a159ef7..61c54b7 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -43,7 +43,8 @@ jobs: apt-get update apt-get install -y git \ cmake make ninja-build sudo \ - python3 python3-pip python3-venv + python3 python3-pip python3-venv \ + libcurl4-gnutls-dev - name: "Run CMake" shell: bash @@ -76,7 +77,8 @@ jobs: sudo apt-get install -y git \ cmake make ninja-build sudo \ python3 python3-pip python3-venv \ - gcc g++ + gcc g++ libcurl4-gnutls-dev + gcc g++ libcurl4-gnutls-dev - name: "Compile tests" run: | diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml index 67c9a6c..4166c32 100644 --- a/.github/workflows/python-bindings.yml +++ b/.github/workflows/python-bindings.yml @@ -33,7 +33,7 @@ jobs: # Install platform build dependencies - name: "Install system packages (Ubuntu)" if: startsWith(matrix.on, 'ubuntu-') - run: sudo apt-get update && sudo apt-get install -y ninja-build g++ cmake + run: sudo apt-get update && sudo apt-get install -y ninja-build g++ cmake libcurl4-gnutls-dev - name: "Setup Homebrew (macOS)" if: startsWith(matrix.on, 'macos-') @@ -41,7 +41,7 @@ jobs: - name: "Install system packages (macOS)" if: startsWith(matrix.on, 'macos-') - run: brew install ninja gcc cmake + run: brew install ninja gcc cmake curl - name: "Create virtual environment" run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c33399..4973a33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,9 @@ endif () # Tests (only when built standalone) ##################################### if (CAPIO_CL_BUILD_TESTS) + + find_package(CURL REQUIRED) + message(STATUS "Building CAPIO-CL tests") FetchContent_Declare( @@ -206,6 +209,7 @@ if (CAPIO_CL_BUILD_TESTS) target_link_libraries(CAPIO_CL_tests PRIVATE libcapio_cl GTest::gtest_main + CURL::libcurl ) if(LIBANL) diff --git a/pyproject.toml b/pyproject.toml index 77b13b6..e884c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,7 @@ requires = [ "scikit-build-core[pyproject]==0.11.6", "pybind11==3.0.1", "setuptools==80.9.0", - "wheel==0.45.1", - "requests==2.32.5" + "wheel==0.45.1" ] build-backend = "scikit_build_core.build" @@ -18,6 +17,9 @@ requires-python = ">=3.10" authors = [ { name = "Marco Edoardo Santimaria", email = "marcoedoardo.santimaria@unito.it" } ] +dependencies = [ + "requests==2.32.5" +] [project.urls] Homepage = "https://capio.hpc4ai.it" diff --git a/tests/cpp/main.cpp b/tests/cpp/main.cpp index 2839dfb..ab3657e 100644 --- a/tests/cpp/main.cpp +++ b/tests/cpp/main.cpp @@ -25,4 +25,5 @@ template std::string demangled_name(const T &obj) { #include "test_engine.hpp" #include "test_exceptions.hpp" #include "test_monitor.hpp" -#include "test_serialize_deserialize.hpp" \ No newline at end of file +#include "test_serialize_deserialize.hpp" +#include "test_apis.hpp" \ No newline at end of file diff --git a/tests/cpp/test_apis.hpp b/tests/cpp/test_apis.hpp new file mode 100644 index 0000000..07c6cf6 --- /dev/null +++ b/tests/cpp/test_apis.hpp @@ -0,0 +1,123 @@ +#ifndef CAPIO_CL_TEST_APIS_HPP +#define CAPIO_CL_TEST_APIS_HPP + +#define WEBSERVER_SUITE_NAME TestWebServerAPIS + +#include + +#include +#include +#include +#include +#include + +enum class HttpMethod { GET, POST, DELETE }; + +static size_t write_cb(const char *ptr, const size_t size, size_t nmemb, void *userdata) { + auto *response = static_cast(userdata); + response->append(ptr, size * nmemb); + return size * nmemb; +} + +static std::unordered_map from_json(const std::string &json) { + std::unordered_map result; + size_t pos = 0; + + while (true) { + auto k1 = json.find('"', pos); + if (k1 == std::string::npos) { + break; + } + const auto k2 = json.find('"', k1 + 1); + + const auto v1 = json.find('"', k2 + 1); + const auto v2 = json.find('"', v1 + 1); + + std::string key = json.substr(k1 + 1, k2 - k1 - 1); + const std::string val = json.substr(v1 + 1, v2 - v1 - 1); + + result[key] = val; + pos = v2 + 1; + } + return result; +} + +inline std::unordered_map +perform_request(const std::string &url, const std::string &request_params_json_encode, + HttpMethod method = HttpMethod::GET) { + CURL *curl = curl_easy_init(); + if (!curl) { + throw std::runtime_error("curl_easy_init failed"); + } + + std::string response; + + curl_slist *headers = nullptr; + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, "Accept: application/json"); + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_params_json_encode.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request_params_json_encode.size()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + switch (method) { + case HttpMethod::GET: + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + break; + + case HttpMethod::POST: + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + break; + + case HttpMethod::DELETE: + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + break; + } + + const CURLcode res = curl_easy_perform(curl); + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + throw std::runtime_error(curl_easy_strerror(res)); + } + if (http_code < 200 || http_code >= 300) { + throw std::runtime_error("HTTP error " + std::to_string(http_code)); + } + + return from_json(response); +} + +TEST(WEBSERVER_SUITE_NAME, testGetAndSetWorkflowName) { + + const auto engine = capiocl::engine::Engine(); + + sleep(1); + + auto response = perform_request("http://localhost:5520/workflow", "{}", HttpMethod::GET); + if (response["name"] != capiocl::CAPIO_CL_DEFAULT_WF_NAME) { + for (const auto &[key, val] : response) { + std::cout << key << " : " << val << std::endl; + } + } + + + EXPECT_FALSE(response.empty()); + EXPECT_TRUE(response["name"] == capiocl::CAPIO_CL_DEFAULT_WF_NAME); + + + perform_request("http://localhost:5520/workflow", R"({"name": "test_workflow_0"})", + HttpMethod::POST); + response = perform_request("http://localhost:5520/workflow", "{}", HttpMethod::GET); + EXPECT_FALSE(response.empty()); + EXPECT_TRUE(response["name"] == "test_workflow_0"); +} + +#endif // CAPIO_CL_TEST_APIS_HPP From 7c3dea7a365a76f33251045b74ef44fea796e99c Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 11:39:22 +0000 Subject: [PATCH 07/19] Test for bind errors --- src/webapi.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webapi.cpp b/src/webapi.cpp index e548e3c..90bd01a 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -245,7 +245,11 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }) }); - _server.listen(address, port); + if (!_server.bind_to_port(address, port)) { + throw std::runtime_error("Could not bind to" + address + "@" + std::to_string(port) + + ". Error is: " + std::strerror(errno)); + } + _server.listen_after_bind(); } capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, From 3fa358b1edc5620116a80d0bf79c872c5d1cce55 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 11:44:16 +0000 Subject: [PATCH 08/19] Test for bind errors --- src/webapi.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/webapi.cpp b/src/webapi.cpp index 90bd01a..f53ba6d 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -250,6 +250,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine ". Error is: " + std::strerror(errno)); } _server.listen_after_bind(); + capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "terminated API webserver"); } capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, From 2e357dd4ca15adf55fba6da82e66072d6b8eb581 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 12:01:18 +0000 Subject: [PATCH 09/19] termination fixes --- capiocl/webapi.h | 4 ---- src/webapi.cpp | 27 ++++++++------------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/capiocl/webapi.h b/capiocl/webapi.h index f3e5a9d..b322c2c 100644 --- a/capiocl/webapi.h +++ b/capiocl/webapi.h @@ -13,10 +13,6 @@ class capiocl::webapi::CapioClWebApiServer { /// @brief port on which the current server runs int _port; - /// @brief secret key initialized at random. used when requesting shutdown so that the - /// termination can only occur when called from the main CAPIO-CL thread - char _secretKey[256]{}; - public: /// @brief default constructor. CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, diff --git a/src/webapi.cpp b/src/webapi.cpp index f53ba6d..331441d 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -39,8 +39,7 @@ } /// @brief Main WebServer thread function -void server(const std::string &address, const int port, capiocl::engine::Engine *engine, - const char secret_term_key[256]) { +void server(const std::string &address, const int port, capiocl::engine::Engine *engine) { capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "Starting API server @ " + address + ":" + std::to_string(port)); @@ -232,16 +231,8 @@ void server(const std::string &address, const int port, capiocl::engine::Engine _server.Get("/terminate", [&]([[maybe_unused]] const httplib::Request &req, [[maybe_unused]] httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { - const auto req_key = request_body["key"].as(); - bool match = true; - for (size_t i = 0; i < req_key.size() && match; i++) { - match = secret_term_key[i] == req_key[i]; - } - - if (match) { - capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "API server stopped"); - _server.stop(); - } + capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "API server stopped"); + _server.stop(); }) }); @@ -250,22 +241,20 @@ void server(const std::string &address, const int port, capiocl::engine::Engine ". Error is: " + std::strerror(errno)); } _server.listen_after_bind(); - capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "terminated API webserver"); } capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, const std::string &web_server_address, const int web_server_port) - : _webApiThread(std::thread(server, web_server_address, web_server_port, engine, _secretKey)), - _port(web_server_port) { - FILE *f = fopen("/dev/urandom", "rb"); - fread(_secretKey, 1, 256, f); - fclose(f); - _webApiThread.detach(); + : _port(web_server_port) { + _webApiThread = std::thread(server, web_server_address, web_server_port, engine); } capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { httplib::Client client("http://127.0.0.1:" + std::to_string(_port)); client.Get("/terminate"); + if (_webApiThread.joinable()) { + _webApiThread.join(); + } } \ No newline at end of file From ba7b5dfd78116ba4de5a492004c83f60672edd0f Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 12:43:19 +0000 Subject: [PATCH 10/19] Added method to explitily start CAPIO-CL api webserver --- bindings/python_bindings.cpp | 2 ++ capiocl/engine.h | 9 ++++++++- src/Engine.cpp | 8 ++++++-- tests/cpp/test_apis.hpp | 8 +++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/bindings/python_bindings.cpp b/bindings/python_bindings.cpp index 91fe5f2..864199d 100644 --- a/bindings/python_bindings.cpp +++ b/bindings/python_bindings.cpp @@ -98,6 +98,8 @@ PYBIND11_MODULE(_py_capio_cl, m) { .def("isCommitted", &capiocl::engine::Engine::isCommitted, py::arg("path")) .def("setHomeNode", &capiocl::engine::Engine::setHomeNode, py::arg("path")) .def("getPaths", &capiocl::engine::Engine::getPaths) + .def("startApiServer", &capiocl::engine::Engine::startApiServer, + py::arg("address") = "127.0.0.1", py::arg("port") = 5520) .def("__str__", &capiocl::engine::Engine::print) .def("__repr__", [](const capiocl::engine::Engine &e) { diff --git a/capiocl/engine.h b/capiocl/engine.h index 4bf83c6..d3b1ae0 100644 --- a/capiocl/engine.h +++ b/capiocl/engine.h @@ -41,7 +41,7 @@ class Engine final { std::string workflow_name; /// @brief CAPIO-CL APIs Web Server - webapi::CapioClWebApiServer webapi_server; + std::unique_ptr webapi_server; // LCOV_EXCL_START /// @brief Internal CAPIO-CL Engine storage entity. Each CapioCLEntry is an entry for a given @@ -408,6 +408,13 @@ class Engine final { * Use default CAPIO-CL TOML configuration. */ void useDefaultConfiguration(); + + /** + * Start the thread involved in the handling of dynamic changes to CapioCl configuration + * @param address address to listen to. defaulto to 127.0.0.1 + * @param port Port to listen to. defaults to 5520 + */ + void startApiServer(const std::string &address = "127.0.0.1", int port = 5520); }; } // namespace capiocl::engine diff --git a/src/Engine.cpp b/src/Engine.cpp index 66e1575..d6a37c2 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -127,8 +127,7 @@ void capiocl::engine::Engine::print() const { printer::print(printer::CLI_LEVEL_JSON, ""); } -capiocl::engine::Engine::Engine(const bool use_default_settings) - : webapi_server(this, "127.0.0.1", 5520) { +capiocl::engine::Engine::Engine(const bool use_default_settings) { node_name = std::string(1024, '\0'); gethostname(node_name.data(), node_name.size()); node_name.resize(std::strlen(node_name.c_str())); @@ -772,4 +771,9 @@ void capiocl::engine::Engine::useDefaultConfiguration() { monitor.registerMonitorBackend(new monitor::MulticastMonitor(def_config)); monitor.registerMonitorBackend(new monitor::FileSystemMonitor()); +} + +void capiocl::engine::Engine::startApiServer(const std::string &address, const int port) { + webapi_server = std::unique_ptr( + new webapi::CapioClWebApiServer(this, address, port)); } \ No newline at end of file diff --git a/tests/cpp/test_apis.hpp b/tests/cpp/test_apis.hpp index 07c6cf6..1b0538b 100644 --- a/tests/cpp/test_apis.hpp +++ b/tests/cpp/test_apis.hpp @@ -97,7 +97,11 @@ perform_request(const std::string &url, const std::string &request_params_json_e TEST(WEBSERVER_SUITE_NAME, testGetAndSetWorkflowName) { - const auto engine = capiocl::engine::Engine(); + //clean environment for wf name + unsetenv("WORKFLOW_NAME"); + + auto engine = capiocl::engine::Engine(); + engine.startApiServer(); sleep(1); @@ -108,11 +112,9 @@ TEST(WEBSERVER_SUITE_NAME, testGetAndSetWorkflowName) { } } - EXPECT_FALSE(response.empty()); EXPECT_TRUE(response["name"] == capiocl::CAPIO_CL_DEFAULT_WF_NAME); - perform_request("http://localhost:5520/workflow", R"({"name": "test_workflow_0"})", HttpMethod::POST); response = perform_request("http://localhost:5520/workflow", "{}", HttpMethod::GET); From b571231258626068a33beb41845bf563c442eff9 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 14:01:14 +0000 Subject: [PATCH 11/19] cleanup --- .github/workflows/ci-test.yml | 1 - tests/cpp/test_apis.hpp | 46 +++++++---------------------------- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 61c54b7..a7b93a2 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -78,7 +78,6 @@ jobs: cmake make ninja-build sudo \ python3 python3-pip python3-venv \ gcc g++ libcurl4-gnutls-dev - gcc g++ libcurl4-gnutls-dev - name: "Compile tests" run: | diff --git a/tests/cpp/test_apis.hpp b/tests/cpp/test_apis.hpp index 1b0538b..2678c34 100644 --- a/tests/cpp/test_apis.hpp +++ b/tests/cpp/test_apis.hpp @@ -3,8 +3,7 @@ #define WEBSERVER_SUITE_NAME TestWebServerAPIS -#include - +#include "jsoncons/json.hpp" #include #include #include @@ -13,38 +12,16 @@ enum class HttpMethod { GET, POST, DELETE }; -static size_t write_cb(const char *ptr, const size_t size, size_t nmemb, void *userdata) { +static size_t curl_write_response_handler(const char *ptr, const size_t size, size_t nmemb, + void *userdata) { auto *response = static_cast(userdata); response->append(ptr, size * nmemb); return size * nmemb; } -static std::unordered_map from_json(const std::string &json) { - std::unordered_map result; - size_t pos = 0; - - while (true) { - auto k1 = json.find('"', pos); - if (k1 == std::string::npos) { - break; - } - const auto k2 = json.find('"', k1 + 1); - - const auto v1 = json.find('"', k2 + 1); - const auto v2 = json.find('"', v1 + 1); - - std::string key = json.substr(k1 + 1, k2 - k1 - 1); - const std::string val = json.substr(v1 + 1, v2 - v1 - 1); - - result[key] = val; - pos = v2 + 1; - } - return result; -} - -inline std::unordered_map -perform_request(const std::string &url, const std::string &request_params_json_encode, - HttpMethod method = HttpMethod::GET) { +inline jsoncons::json perform_request(const std::string &url, + const std::string &request_params_json_encode, + HttpMethod method = HttpMethod::GET) { CURL *curl = curl_easy_init(); if (!curl) { throw std::runtime_error("curl_easy_init failed"); @@ -60,7 +37,7 @@ perform_request(const std::string &url, const std::string &request_params_json_e curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_params_json_encode.c_str()); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request_params_json_encode.size()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_response_handler); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); switch (method) { @@ -92,12 +69,12 @@ perform_request(const std::string &url, const std::string &request_params_json_e throw std::runtime_error("HTTP error " + std::to_string(http_code)); } - return from_json(response); + return jsoncons::json::parse(std::string(response)); } TEST(WEBSERVER_SUITE_NAME, testGetAndSetWorkflowName) { - //clean environment for wf name + // clean environment for wf name unsetenv("WORKFLOW_NAME"); auto engine = capiocl::engine::Engine(); @@ -106,11 +83,6 @@ TEST(WEBSERVER_SUITE_NAME, testGetAndSetWorkflowName) { sleep(1); auto response = perform_request("http://localhost:5520/workflow", "{}", HttpMethod::GET); - if (response["name"] != capiocl::CAPIO_CL_DEFAULT_WF_NAME) { - for (const auto &[key, val] : response) { - std::cout << key << " : " << val << std::endl; - } - } EXPECT_FALSE(response.empty()); EXPECT_TRUE(response["name"] == capiocl::CAPIO_CL_DEFAULT_WF_NAME); From 8d8f40f3e2a438951b036a57ebd125b728337346 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 14:39:29 +0000 Subject: [PATCH 12/19] more tests --- bruno_webapi_tests/set_excluded.bru | 2 +- src/webapi.cpp | 2 +- tests/cpp/test_apis.hpp | 128 +++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 3 deletions(-) diff --git a/bruno_webapi_tests/set_excluded.bru b/bruno_webapi_tests/set_excluded.bru index 68af243..55676e4 100644 --- a/bruno_webapi_tests/set_excluded.bru +++ b/bruno_webapi_tests/set_excluded.bru @@ -13,7 +13,7 @@ post { body:json { { "path" : "/tmp/test.txt", - "excluded" : true + "exclude" : true } } diff --git a/src/webapi.cpp b/src/webapi.cpp index 331441d..bf60eb9 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -180,7 +180,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine _server.Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); - const auto excluded = request_body["excluded"].as(); + const auto excluded = request_body["exclude"].as(); engine->setExclude(path, excluded); }); }); diff --git a/tests/cpp/test_apis.hpp b/tests/cpp/test_apis.hpp index 2678c34..5f5e720 100644 --- a/tests/cpp/test_apis.hpp +++ b/tests/cpp/test_apis.hpp @@ -68,7 +68,7 @@ inline jsoncons::json perform_request(const std::string &url, if (http_code < 200 || http_code >= 300) { throw std::runtime_error("HTTP error " + std::to_string(http_code)); } - + std::cout << "DBG RES: " << response << std::endl; return jsoncons::json::parse(std::string(response)); } @@ -94,4 +94,130 @@ TEST(WEBSERVER_SUITE_NAME, testGetAndSetWorkflowName) { EXPECT_TRUE(response["name"] == "test_workflow_0"); } +TEST(WEBSERVER_SUITE_NAME, consumer) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = + perform_request("http://localhost:5520/consumer", + R"({"path" : "/tmp/test.txt", "consumer" : "sample2"})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/consumer", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["consumers"][0] == "sample2"); +} + +TEST(WEBSERVER_SUITE_NAME, producer) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = + perform_request("http://localhost:5520/producer", + R"({"path" : "/tmp/test.txt", "producer" : "sample1"})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/producer", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["producers"][0] == "sample1"); +} + +TEST(WEBSERVER_SUITE_NAME, commit) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = + perform_request("http://localhost:5520/commit", + R"({"path" : "/tmp/test.txt","commit" : "on_file"})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/commit", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["commit"] == "on_file"); +} + +TEST(WEBSERVER_SUITE_NAME, fire) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = + perform_request("http://localhost:5520/fire", + R"({"path" : "/tmp/test.txt","fire" : "no_update"})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/fire", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["fire"] == "no_update"); +} + +TEST(WEBSERVER_SUITE_NAME, fileDependency) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = perform_request("http://localhost:5520/dependency", + R"({"path" : "/tmp/test.txt", "dependency" : "myFile.dat"})", + HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/dependency", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["dependencies"][0] == "myFile.dat"); +} + +TEST(WEBSERVER_SUITE_NAME, on_n_files) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = perform_request("http://localhost:5520/commit/file-count", + R"({"path" : "/tmp/test.txt","count" : 7892})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/commit/file-count", + R"({"path" : "/tmp/test.txt"})", HttpMethod::GET); + EXPECT_EQ(result["count"], 7892); +} + +TEST(WEBSERVER_SUITE_NAME, close_count) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = + perform_request("http://localhost:5520/commit/close-count", + R"({"path" : "/tmp/test.txt","count" : 12345})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/commit/close-count", + R"({"path" : "/tmp/test.txt"})", HttpMethod::GET); + EXPECT_EQ(result["count"], 12345); +} + +TEST(WEBSERVER_SUITE_NAME, boolean_flag) { + + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = + perform_request("http://localhost:5520/permanent", + R"({"path" : "/tmp/test.txt","permanent" : true})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/permanent", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["permanent"].as_bool()); + + result = perform_request("http://localhost:5520/exclude", + R"({"path" : "/tmp/test.txt","exclude" : true})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/exclude", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["exclude"].as_bool()); + + result = perform_request("http://localhost:5520/directory", + R"({"path" : "/tmp/test.txt","directory" : true})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/directory", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_TRUE(result["directory"].as_bool()); +} + #endif // CAPIO_CL_TEST_APIS_HPP From 8bbcad21da66fafd40074b8bbd366e0600097fca Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 15:15:24 +0000 Subject: [PATCH 13/19] refactor --- README.md | 439 +++++++++++++++++- bruno_webapi_tests/add_consumer.bru | 2 +- bruno_webapi_tests/add_file_dependency.bru | 2 +- bruno_webapi_tests/add_producer.bru | 2 +- bruno_webapi_tests/get_commit_close_count.bru | 2 +- .../get_commit_on_n_files_count.bru | 2 +- bruno_webapi_tests/get_commit_rule.bru | 2 +- bruno_webapi_tests/get_consumer.bru | 2 +- bruno_webapi_tests/get_directory.bru | 2 +- bruno_webapi_tests/get_excluded.bru | 2 +- bruno_webapi_tests/get_file_dependencies.bru | 2 +- bruno_webapi_tests/get_fire_rule.bru | 2 +- bruno_webapi_tests/get_permanent.bru | 2 +- bruno_webapi_tests/get_producer.bru | 2 +- bruno_webapi_tests/get_workflow_name.bru | 2 +- bruno_webapi_tests/remove_file.bru | 22 - bruno_webapi_tests/set_commit_close_count.bru | 2 +- .../set_commit_on_n_files_count.bru | 2 +- bruno_webapi_tests/set_commit_rule.bru | 2 +- bruno_webapi_tests/set_directory.bru | 2 +- bruno_webapi_tests/set_excluded.bru | 2 +- bruno_webapi_tests/set_fire_rule.bru | 2 +- bruno_webapi_tests/set_permanent.bru | 2 +- src/webapi.cpp | 20 +- tests/cpp/main.cpp | 4 +- 25 files changed, 456 insertions(+), 71 deletions(-) delete mode 100644 bruno_webapi_tests/remove_file.bru diff --git a/README.md b/README.md index 9cf0c71..f4579b5 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,25 @@ ![C++](https://img.shields.io/badge/C%2B%2B-%E2%89%A517-blueviolet?logo=c%2B%2B&logoColor=white) ![Python Bindings](https://img.shields.io/badge/Python_Bindings-3.10–3.14-darkgreen?style=flat&logo=python&logoColor=white&labelColor=gray) - #### Platform support -| OS / Arch | ![x86_64](https://img.shields.io/badge/x86__64-121212?logo=intel&logoColor=blue) | ![ARM](https://img.shields.io/badge/ARM-121212?logo=arm&logoColor=0091BD) | ![RISC-V](https://img.shields.io/badge/RISC--V-121212?logo=riscv&logoColor=F9A825) | -|-----------|----------------------------------------------------------------------------------|---------------------------------------------------------------------------|------------------------------------------------------------------------------------| +| OS / Arch | ![x86_64](https://img.shields.io/badge/x86__64-121212?logo=intel&logoColor=blue) | ![ARM](https://img.shields.io/badge/ARM-121212?logo=arm&logoColor=0091BD) | ![RISC-V](https://img.shields.io/badge/RISC--V-121212?logo=riscv&logoColor=F9A825) | +|------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|---------------------------------------------------------------------------|------------------------------------------------------------------------------------| | ![Ubuntu](https://img.shields.io/badge/Ubuntu-121212?logo=ubuntu&logoColor=E95420) | YES | YES | YES | | ![macOS](https://img.shields.io/badge/macOS-121212?logo=apple&logoColor=white) | YES | YES | N.A. | - #### Documentation - [![Core Language](https://img.shields.io/badge/Core%20Language-10.1007%2Fs10766--025--00789--0-%23cc5500?logo=doi&logoColor=white&labelColor=2b2b2b)](https://doi.org/10.1007/s10766-025-00789-0) - [![Metadata Streaming](https://img.shields.io/badge/Metadata%20Streaming-10.1145%2F3731599.3767577-%23cc5500?logo=doi&logoColor=white&labelColor=2b2b2b)](https://doi.org/10.1145/3731599.3767577) - [![Doxygen documentation](https://img.shields.io/github/v/release/High-Performance-IO/CAPIO-CL?label=Doxygen%20documentation&labelColor=2b2b2b&color=brown&logo=readthedocs&logoColor=white)](https://github.com/High-Performance-IO/CAPIO-CL/releases/latest/download/documentation.pdf) - **CAPIO-CL** is a novel I/O coordination language that enables users to annotate file-based workflow data dependencies with **synchronization semantics** for files and directories. Designed to facilitate **transparent overlap between computation and I/O operations**, CAPIO-CL allows multiple producer–consumer application modules to coordinate efficiently using a **JSON-based syntax**. -For detailed documentation and examples, please visit: +For detailed documentation and examples, please visit: [![CAPIO Website](https://img.shields.io/badge/CAPIO%20Website-Documentation-brightgreen?logo=readthedocs&logoColor=white)](https://capio.hpc4ai.it/docs/coord-language/) @@ -184,7 +181,435 @@ engine.print() Serializer.dump(engine, "my_workflow", "my_workflow.json") ``` -# Team +# CapioCL Web API Documentation + +## Overview + +This section describes the REST-style Web API exposed by the CapioCL Web Server. +The server provides HTTP endpoints for configuring and querying the CapioCL engine at runtime. + +All endpoints communicate using JSON over HTTP. To enable the webserver, users needs to explicitly start it with: + +```cpp +capiocl::engine::Engine engine(); + +// start engine with default parameters +engine.startApiServer(); + +// or by specifying the address and port: +engine.startApiServer("127.0.0.1", 5520); +``` + + +or equivalently in python with: + +```python +engine = py_capio_cl.Engine() + +#start engine with default parameters +engine.startApiServer() + +# or by specifying the address and port: +engine.startApiServer("127.0.0.1", 5520) +``` + +By default, the webserver listens only on local connection at the following address: ```127.0.0.1:5520```. No +authentication +services are currently available, and as such, users should put particular care when allowing connections from external +endpoints. + +--- + +## Common Behavior + +### Content Type + +All requests and responses use: + +``` +Content-Type: application/json +``` + +### Error Handling + +If a request body is invalid or missing required fields, the server responds with: + +```json +{ + "status": "error", + "what": "Invalid request BODY data:
" +} +``` + +HTTP status code: **400** + +### Success Responses + +For POST endpoints: + +```json +{ + "status": "OK" +} +``` + +For GET endpoints, a JSON object with the requested data is returned. + +HTTP status code: **200** + +--- + +## API Endpoints + +### POST /producer + +Registers a producer for a given path. + +**Request Body** + +```json +{ + "path": "string", + "producer": "string" +} +``` + +**Example** + +```bash +curl -X POST http://localhost:PORT/producer \ + -H "Content-Type: application/json" \ + -d '{"path":"src/file.cpp","producer":"compile"}' +``` + +--- + +### GET /producer + +Returns all producers associated with a path. + +**Request Body** + +```json +{ + "path": "string" +} +``` + +**Response** + +```json +{ + "producers": [ + "compile", + "link" + ] +} +``` + +--- + +### POST /consumer + +Registers a consumer for a given path. + +**Request Body** + +```json +{ + "path": "string", + "consumer": "string" +} +``` + +--- + +### GET /consumer + +Returns all consumers associated with a path. + +**Response** + +```json +{ + "consumers": [ + "test", + "package" + ] +} +``` + +--- + +### POST /dependency + +Adds a file dependency for a path. + +**Request Body** + +```json +{ + "path": "string", + "dependency": "relative/or/absolute/path" +} +``` + +--- + +### GET /dependency + +Returns file dependencies that trigger commits. + +**Response** + +```json +{ + "dependencies": [ + "file1.cpp", + "file2.hpp" + ] +} +``` + +--- + +### POST /commit + +Sets the commit rule for a path. + +**Request Body** + +```json +{ + "path": "string", + "commit": "rule-expression" +} +``` + +--- + +### GET /commit + +Gets the commit rule for a path. + +**Response** + +```json +{ + "commit": "rule-expression" +} +``` + +--- + +### POST /commit/file-count + +Sets the file count required to commit a directory. + +**Request Body** + +```json +{ + "path": "string", + "count": 5 +} +``` + +--- + +### GET /commit/file-count + +Returns the directory file count. + +**Response** + +```json +{ + "count": 5 +} +``` + +--- + +### POST /commit/close-count + +Sets the close count required to commit. + +**Request Body** + +```json +{ + "path": "string", + "count": 2 +} +``` + +--- + +### GET /commit/close-count + +Returns the commit close count. + +**Response** + +```json +{ + "count": 2 +} +``` + +--- + +### POST /fire + +Sets the fire rule for a path. + +**Request Body** + +```json +{ + "path": "string", + "fire": "rule-expression" +} +``` + +--- + +### GET /fire + +Returns the fire rule. + +**Response** + +```json +{ + "fire": "rule-expression" +} +``` + +--- + +### POST /permanent + +Marks a path as permanent or temporary. + +**Request Body** + +```json +{ + "path": "string", + "permanent": true +} +``` + +--- + +### GET /permanent + +Returns permanent status. + +**Response** + +```json +{ + "permanent": true +} +``` + +--- + +### POST /exclude + +Marks a path as excluded or included. + +**Request Body** + +```json +{ + "path": "string", + "exclude": true +} +``` + +--- + +### GET /exclude + +Returns exclusion status. + +**Response** + +```json +{ + "exclude": false +} +``` + +--- + +### POST /directory + +Marks a path as a directory or file. + +**Request Body** + +```json +{ + "path": "string", + "directory": true +} +``` + +--- + +### GET /directory + +Returns directory status. + +**Response** + +```json +{ + "directory": true +} +``` + +--- + +### POST /workflow + +Sets the workflow name. + +**Request Body** + +```json +{ + "name": "build-and-test" +} +``` + +--- + +### GET /workflow + +Returns the workflow name. + +**Response** + +```json +{ + "name": "build-and-test" +} +``` + +--- + +## Notes + +- All GET endpoints expect a JSON body containing the targeted file path. +- The API is intended for local control and orchestration, not public exposure. + +--- ## Developing team diff --git a/bruno_webapi_tests/add_consumer.bru b/bruno_webapi_tests/add_consumer.bru index 2deebc8..8a1db14 100644 --- a/bruno_webapi_tests/add_consumer.bru +++ b/bruno_webapi_tests/add_consumer.bru @@ -1,7 +1,7 @@ meta { name: add_consumer type: http - seq: 3 + seq: 2 } post { diff --git a/bruno_webapi_tests/add_file_dependency.bru b/bruno_webapi_tests/add_file_dependency.bru index aea1c74..20bccbd 100644 --- a/bruno_webapi_tests/add_file_dependency.bru +++ b/bruno_webapi_tests/add_file_dependency.bru @@ -1,7 +1,7 @@ meta { name: add_file_dependency type: http - seq: 5 + seq: 4 } post { diff --git a/bruno_webapi_tests/add_producer.bru b/bruno_webapi_tests/add_producer.bru index 9a5db42..8165600 100644 --- a/bruno_webapi_tests/add_producer.bru +++ b/bruno_webapi_tests/add_producer.bru @@ -1,7 +1,7 @@ meta { name: add_producer type: http - seq: 4 + seq: 3 } post { diff --git a/bruno_webapi_tests/get_commit_close_count.bru b/bruno_webapi_tests/get_commit_close_count.bru index 8cf6d38..49a1ed3 100644 --- a/bruno_webapi_tests/get_commit_close_count.bru +++ b/bruno_webapi_tests/get_commit_close_count.bru @@ -1,7 +1,7 @@ meta { name: get_commit_close_count type: http - seq: 19 + seq: 18 } get { diff --git a/bruno_webapi_tests/get_commit_on_n_files_count.bru b/bruno_webapi_tests/get_commit_on_n_files_count.bru index fcd8e32..dbc6e2b 100644 --- a/bruno_webapi_tests/get_commit_on_n_files_count.bru +++ b/bruno_webapi_tests/get_commit_on_n_files_count.bru @@ -1,7 +1,7 @@ meta { name: get_commit_on_n_files_count type: http - seq: 18 + seq: 17 } get { diff --git a/bruno_webapi_tests/get_commit_rule.bru b/bruno_webapi_tests/get_commit_rule.bru index 17ebf94..3950caf 100644 --- a/bruno_webapi_tests/get_commit_rule.bru +++ b/bruno_webapi_tests/get_commit_rule.bru @@ -1,7 +1,7 @@ meta { name: get_commit_rule type: http - seq: 17 + seq: 16 } get { diff --git a/bruno_webapi_tests/get_consumer.bru b/bruno_webapi_tests/get_consumer.bru index 4a1fed0..8b13fba 100644 --- a/bruno_webapi_tests/get_consumer.bru +++ b/bruno_webapi_tests/get_consumer.bru @@ -1,7 +1,7 @@ meta { name: get_consumer type: http - seq: 15 + seq: 14 } get { diff --git a/bruno_webapi_tests/get_directory.bru b/bruno_webapi_tests/get_directory.bru index 213cf03..50c9b5c 100644 --- a/bruno_webapi_tests/get_directory.bru +++ b/bruno_webapi_tests/get_directory.bru @@ -1,7 +1,7 @@ meta { name: get_directory type: http - seq: 23 + seq: 22 } get { diff --git a/bruno_webapi_tests/get_excluded.bru b/bruno_webapi_tests/get_excluded.bru index d347f10..56ad262 100644 --- a/bruno_webapi_tests/get_excluded.bru +++ b/bruno_webapi_tests/get_excluded.bru @@ -1,7 +1,7 @@ meta { name: get_excluded type: http - seq: 22 + seq: 21 } get { diff --git a/bruno_webapi_tests/get_file_dependencies.bru b/bruno_webapi_tests/get_file_dependencies.bru index b641eb2..cff2964 100644 --- a/bruno_webapi_tests/get_file_dependencies.bru +++ b/bruno_webapi_tests/get_file_dependencies.bru @@ -1,7 +1,7 @@ meta { name: get_file_dependencies type: http - seq: 16 + seq: 15 } get { diff --git a/bruno_webapi_tests/get_fire_rule.bru b/bruno_webapi_tests/get_fire_rule.bru index 7bd3a5a..fe4845f 100644 --- a/bruno_webapi_tests/get_fire_rule.bru +++ b/bruno_webapi_tests/get_fire_rule.bru @@ -1,7 +1,7 @@ meta { name: get_fire_rule type: http - seq: 20 + seq: 19 } get { diff --git a/bruno_webapi_tests/get_permanent.bru b/bruno_webapi_tests/get_permanent.bru index 67eabde..87f3d53 100644 --- a/bruno_webapi_tests/get_permanent.bru +++ b/bruno_webapi_tests/get_permanent.bru @@ -1,7 +1,7 @@ meta { name: get_permanent type: http - seq: 21 + seq: 20 } get { diff --git a/bruno_webapi_tests/get_producer.bru b/bruno_webapi_tests/get_producer.bru index 1bdb053..a612e47 100644 --- a/bruno_webapi_tests/get_producer.bru +++ b/bruno_webapi_tests/get_producer.bru @@ -1,7 +1,7 @@ meta { name: get_producer type: http - seq: 14 + seq: 13 } get { diff --git a/bruno_webapi_tests/get_workflow_name.bru b/bruno_webapi_tests/get_workflow_name.bru index 7855d58..6270aa1 100644 --- a/bruno_webapi_tests/get_workflow_name.bru +++ b/bruno_webapi_tests/get_workflow_name.bru @@ -1,7 +1,7 @@ meta { name: get_workflow_name type: http - seq: 13 + seq: 12 } get { diff --git a/bruno_webapi_tests/remove_file.bru b/bruno_webapi_tests/remove_file.bru deleted file mode 100644 index c92023c..0000000 --- a/bruno_webapi_tests/remove_file.bru +++ /dev/null @@ -1,22 +0,0 @@ -meta { - name: remove_file - type: http - seq: 24 -} - -delete { - url: http://localhost:5520 - body: json - auth: inherit -} - -body:json { - { - "path" : "/tmp/test.txt" - } -} - -settings { - encodeUrl: true - timeout: 0 -} diff --git a/bruno_webapi_tests/set_commit_close_count.bru b/bruno_webapi_tests/set_commit_close_count.bru index 5b61861..bf4e62d 100644 --- a/bruno_webapi_tests/set_commit_close_count.bru +++ b/bruno_webapi_tests/set_commit_close_count.bru @@ -1,7 +1,7 @@ meta { name: set_commit_close_count type: http - seq: 8 + seq: 7 } post { diff --git a/bruno_webapi_tests/set_commit_on_n_files_count.bru b/bruno_webapi_tests/set_commit_on_n_files_count.bru index 5c6531e..b0a50f2 100644 --- a/bruno_webapi_tests/set_commit_on_n_files_count.bru +++ b/bruno_webapi_tests/set_commit_on_n_files_count.bru @@ -1,7 +1,7 @@ meta { name: set_commit_on_n_files_count type: http - seq: 7 + seq: 6 } post { diff --git a/bruno_webapi_tests/set_commit_rule.bru b/bruno_webapi_tests/set_commit_rule.bru index c95674c..c12c188 100644 --- a/bruno_webapi_tests/set_commit_rule.bru +++ b/bruno_webapi_tests/set_commit_rule.bru @@ -1,7 +1,7 @@ meta { name: set_commit_rule type: http - seq: 6 + seq: 5 } post { diff --git a/bruno_webapi_tests/set_directory.bru b/bruno_webapi_tests/set_directory.bru index ecc2353..6dd873c 100644 --- a/bruno_webapi_tests/set_directory.bru +++ b/bruno_webapi_tests/set_directory.bru @@ -1,7 +1,7 @@ meta { name: set_directory type: http - seq: 12 + seq: 11 } post { diff --git a/bruno_webapi_tests/set_excluded.bru b/bruno_webapi_tests/set_excluded.bru index 55676e4..72584db 100644 --- a/bruno_webapi_tests/set_excluded.bru +++ b/bruno_webapi_tests/set_excluded.bru @@ -1,7 +1,7 @@ meta { name: set_excluded type: http - seq: 11 + seq: 10 } post { diff --git a/bruno_webapi_tests/set_fire_rule.bru b/bruno_webapi_tests/set_fire_rule.bru index 69e9022..6edad97 100644 --- a/bruno_webapi_tests/set_fire_rule.bru +++ b/bruno_webapi_tests/set_fire_rule.bru @@ -1,7 +1,7 @@ meta { name: set_fire_rule type: http - seq: 9 + seq: 8 } post { diff --git a/bruno_webapi_tests/set_permanent.bru b/bruno_webapi_tests/set_permanent.bru index 03ac5b1..1505180 100644 --- a/bruno_webapi_tests/set_permanent.bru +++ b/bruno_webapi_tests/set_permanent.bru @@ -1,7 +1,7 @@ meta { name: set_permanent type: http - seq: 10 + seq: 9 } post { diff --git a/src/webapi.cpp b/src/webapi.cpp index bf60eb9..af116c1 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -46,13 +46,6 @@ void server(const std::string &address, const int port, capiocl::engine::Engine httplib::Server _server; - _server.Post("/new", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { - const auto path = request_body["path"].as(); - engine->newFile(path); - }) - }); - _server.Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { PROCESS_POST_REQUEST(req, res, { const auto path = request_body["path"].as(); @@ -221,13 +214,6 @@ void server(const std::string &address, const int port, capiocl::engine::Engine PROCESS_GET_REQUEST(req, res, { reply["name"] = engine->getWorkflowName(); }); }); - _server.Delete("/", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { - const auto path = request_body["path"].as(); - engine->remove(path); - }); - }); - _server.Get("/terminate", [&]([[maybe_unused]] const httplib::Request &req, [[maybe_unused]] httplib::Response &res) { PROCESS_GET_REQUEST(req, res, { @@ -236,11 +222,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }) }); - if (!_server.bind_to_port(address, port)) { - throw std::runtime_error("Could not bind to" + address + "@" + std::to_string(port) + - ". Error is: " + std::strerror(errno)); - } - _server.listen_after_bind(); + _server.listen(address, port); } capiocl::webapi::CapioClWebApiServer::CapioClWebApiServer(engine::Engine *engine, diff --git a/tests/cpp/main.cpp b/tests/cpp/main.cpp index ab3657e..29f742f 100644 --- a/tests/cpp/main.cpp +++ b/tests/cpp/main.cpp @@ -21,9 +21,9 @@ template std::string demangled_name(const T &obj) { #include "capiocl/printer.h" #include "capiocl/serializer.h" +#include "test_apis.hpp" #include "test_configuration.hpp" #include "test_engine.hpp" #include "test_exceptions.hpp" #include "test_monitor.hpp" -#include "test_serialize_deserialize.hpp" -#include "test_apis.hpp" \ No newline at end of file +#include "test_serialize_deserialize.hpp" \ No newline at end of file From d1908f8feed311563d3d11b07f414207a0c29eda Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 15:17:10 +0000 Subject: [PATCH 14/19] readme --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index f4579b5..5e6bb00 100644 --- a/README.md +++ b/README.md @@ -181,9 +181,7 @@ engine.print() Serializer.dump(engine, "my_workflow", "my_workflow.json") ``` -# CapioCL Web API Documentation - -## Overview +## CapioCL Web API Documentation This section describes the REST-style Web API exposed by the CapioCL Web Server. The server provides HTTP endpoints for configuring and querying the CapioCL engine at runtime. @@ -220,8 +218,6 @@ endpoints. --- -## Common Behavior - ### Content Type All requests and responses use: @@ -259,8 +255,6 @@ HTTP status code: **200** --- -## API Endpoints - ### POST /producer Registers a producer for a given path. From 89c979f855e05aebcc003f7b70f81b855df61c9f Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 15:43:24 +0000 Subject: [PATCH 15/19] From macro to template for better coverage --- src/webapi.cpp | 125 +++++++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/src/webapi.cpp b/src/webapi.cpp index af116c1..5e16d63 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -5,38 +5,47 @@ #include "capiocl/printer.h" #include "capiocl/webapi.h" -#define OK_RESPONSE(res) \ - res.status = 200; \ - res.set_content("{\"status\" : \"OK\"}", "application/json"); - -#define ERROR_RESPONSE(res, e) \ - res.status = 400; \ - res.set_content("{\"status\" : \"error\", \"what\" : \"" + \ - std::string("Invalid request BODY data: ") + e.what() + "\"}", \ +template void ok_response(Res &res) { + res.status = 200; + res.set_content(R"({"status" : "OK"})", "application/json"); +} + +template void error_response(Res &res, const std::exception &e) { + res.status = 400; + res.set_content(std::string(R"({"status" : "error", "what" : ")") + + "Invalid request BODY data: " + e.what() + "\"}", "application/json"); +} + +template void json_response(Res &res, const jsoncons::json &body) { + res.status = 200; + res.set_content(body.as_string(), "application/json"); +} -#define JSON_RESPONSE(res, json_body) \ - res.status = 200; \ - res.set_content(json_body.as_string(), "application/json"); - -#define PROCESS_POST_REQUEST(req, res, code) \ - try { \ - jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); \ - code; \ - OK_RESPONSE(res); \ - } catch (const std::exception &e) { \ - ERROR_RESPONSE(res, e); \ +template +void process_post_request(const Req &req, Res &res, Fn &&handler) { + try { + jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); + + handler(request_body); + ok_response(res); + } catch (const std::exception &e) { + error_response(res, e); } +} -#define PROCESS_GET_REQUEST(req, res, code) \ - try { \ - jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); \ - jsoncons::json reply; \ - code; \ - JSON_RESPONSE(res, reply); \ - } catch (const std::exception &e) { \ - ERROR_RESPONSE(res, e); \ +template +void process_get_request(const Req &req, Res &res, Fn &&handler) { + try { + jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); + + jsoncons::json reply; + handler(request_body, reply); + json_response(res, reply); + } catch (const std::exception &e) { + error_response(res, e); } +} /// @brief Main WebServer thread function void server(const std::string &address, const int port, capiocl::engine::Engine *engine) { @@ -47,7 +56,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine httplib::Server _server; _server.Post("/producer", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto producer = request_body["producer"].as(); engine->addProducer(path, producer); @@ -55,14 +64,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/producer", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["producers"] = engine->getProducers(path); }); }); _server.Post("/consumer", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto consumer = request_body["consumer"].as(); engine->addConsumer(path, consumer); @@ -70,14 +79,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/consumer", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["consumers"] = engine->getConsumers(path); }); }); _server.Post("/dependency", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto dependency = std::filesystem::path(request_body["dependency"].as()); engine->addFileDependency(path, dependency); @@ -85,18 +94,18 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/dependency", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); std::vector deps; - for (const auto file : engine->getCommitOnFileDependencies(path)) { - deps.emplace_back(file.string()); + for (const auto& file : engine->getCommitOnFileDependencies(path)) { + deps.emplace_back(file); } reply["dependencies"] = deps; }); }); _server.Post("/commit", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto commit_rule = request_body["commit"].as(); engine->setCommitRule(path, commit_rule); @@ -104,14 +113,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/commit", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["commit"] = engine->getCommitRule(path); }); }); _server.Post("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto count = request_body["count"].as(); engine->setDirectoryFileCount(path, count); @@ -119,14 +128,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/commit/file-count", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["count"] = engine->getDirectoryFileCount(path); }); }); _server.Post("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto count = request_body["count"].as(); engine->setCommitedCloseNumber(path, count); @@ -134,14 +143,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/commit/close-count", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["count"] = engine->getCommitCloseCount(path); }); }); _server.Post("/fire", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); auto fire_rule = request_body["fire"].as(); engine->setFireRule(path, fire_rule); @@ -149,14 +158,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/fire", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["fire"] = engine->getFireRule(path); }); }); _server.Post("/permanent", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); const auto permanent = request_body["permanent"].as(); engine->setPermanent(path, permanent); @@ -164,14 +173,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/permanent", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["permanent"] = engine->isPermanent(path); }); }); _server.Post("/exclude", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); const auto excluded = request_body["exclude"].as(); engine->setExclude(path, excluded); @@ -179,14 +188,14 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/exclude", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["exclude"] = engine->isExcluded(path); }); }); _server.Post("/directory", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto path = request_body["path"].as(); if (request_body["directory"].as()) { engine->setDirectory(path); @@ -197,29 +206,35 @@ void server(const std::string &address, const int port, capiocl::engine::Engine }); _server.Get("/directory", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { + process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); reply["directory"] = engine->isDirectory(path); }); }); _server.Post("/workflow", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_POST_REQUEST(req, res, { + process_post_request(req, res, [&](jsoncons::json &request_body) { const auto workflow_name = request_body["name"].as(); engine->setWorkflowName(workflow_name); }); }); _server.Get("/workflow", [&](const httplib::Request &req, httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { reply["name"] = engine->getWorkflowName(); }); + process_get_request( + req, res, [&]([[maybe_unused]] jsoncons::json &request_body, jsoncons::json &reply) { + reply["name"] = engine->getWorkflowName(); + }); }); _server.Get("/terminate", [&]([[maybe_unused]] const httplib::Request &req, [[maybe_unused]] httplib::Response &res) { - PROCESS_GET_REQUEST(req, res, { - capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, "API server stopped"); - _server.stop(); - }) + process_get_request(req, res, + [&]([[maybe_unused]] jsoncons::json &request_body, + [[maybe_unused]] jsoncons::json &reply) { + capiocl::printer::print(capiocl::printer::CLI_LEVEL_INFO, + "API server stopped"); + _server.stop(); + }); }); _server.listen(address, port); From 22962a8817f8b192f5f61eeb89c20b568177dbc1 Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 16:04:07 +0000 Subject: [PATCH 16/19] More tests --- src/webapi.cpp | 2 +- tests/cpp/test_apis.hpp | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/webapi.cpp b/src/webapi.cpp index 5e16d63..5a1ad79 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -97,7 +97,7 @@ void server(const std::string &address, const int port, capiocl::engine::Engine process_get_request(req, res, [&](jsoncons::json &request_body, jsoncons::json &reply) { const auto path = request_body["path"].as(); std::vector deps; - for (const auto& file : engine->getCommitOnFileDependencies(path)) { + for (const auto &file : engine->getCommitOnFileDependencies(path)) { deps.emplace_back(file); } reply["dependencies"] = deps; diff --git a/tests/cpp/test_apis.hpp b/tests/cpp/test_apis.hpp index 5f5e720..c3312b4 100644 --- a/tests/cpp/test_apis.hpp +++ b/tests/cpp/test_apis.hpp @@ -65,9 +65,7 @@ inline jsoncons::json perform_request(const std::string &url, if (res != CURLE_OK) { throw std::runtime_error(curl_easy_strerror(res)); } - if (http_code < 200 || http_code >= 300) { - throw std::runtime_error("HTTP error " + std::to_string(http_code)); - } + std::cout << "DBG RES: " << response << std::endl; return jsoncons::json::parse(std::string(response)); } @@ -191,6 +189,16 @@ TEST(WEBSERVER_SUITE_NAME, close_count) { EXPECT_EQ(result["count"], 12345); } +TEST(WEBSERVER_SUITE_NAME, test_error) { + capiocl::engine::Engine engine; + engine.startApiServer(); + sleep(1); + + auto result = perform_request("http://localhost:5520/commit", R"({})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "error"); + EXPECT_GT(result["what"].as_string().size(), 0); +} + TEST(WEBSERVER_SUITE_NAME, boolean_flag) { capiocl::engine::Engine engine; @@ -218,6 +226,13 @@ TEST(WEBSERVER_SUITE_NAME, boolean_flag) { result = perform_request("http://localhost:5520/directory", R"({"path" : "/tmp/test.txt"})", HttpMethod::GET); EXPECT_TRUE(result["directory"].as_bool()); + + result = perform_request("http://localhost:5520/directory", + R"({"path" : "/tmp/test.txt","directory" : false})", HttpMethod::POST); + EXPECT_TRUE(result["status"] == "OK"); + result = perform_request("http://localhost:5520/directory", R"({"path" : "/tmp/test.txt"})", + HttpMethod::GET); + EXPECT_FALSE(result["directory"].as_bool()); } #endif // CAPIO_CL_TEST_APIS_HPP From 8951fabf570708ffcd21850d9610552155df0bbc Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 16:34:19 +0000 Subject: [PATCH 17/19] Code cleanup --- src/webapi.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/webapi.cpp b/src/webapi.cpp index 5a1ad79..f9701c9 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -25,7 +25,7 @@ template void json_response(Res &res, const jsoncons::json &body) template void process_post_request(const Req &req, Res &res, Fn &&handler) { try { - jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); + jsoncons::json request_body = jsoncons::json::parse(req.body); handler(request_body); ok_response(res); @@ -37,7 +37,7 @@ void process_post_request(const Req &req, Res &res, Fn &&handler) { template void process_get_request(const Req &req, Res &res, Fn &&handler) { try { - jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); + jsoncons::json request_body = jsoncons::json::parse(req.body); jsoncons::json reply; handler(request_body, reply); @@ -253,5 +253,7 @@ capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { client.Get("/terminate"); if (_webApiThread.joinable()) { _webApiThread.join(); + }else { + return; } } \ No newline at end of file From d1d7137364039492e098f9a3c4eb46b613b1402b Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria <39337626+marcoSanti@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:02:32 +0000 Subject: [PATCH 18/19] Fix JSON parsing for empty request body Handle empty request body by parsing an empty JSON object. --- src/webapi.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/webapi.cpp b/src/webapi.cpp index f9701c9..6439f90 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -25,7 +25,7 @@ template void json_response(Res &res, const jsoncons::json &body) template void process_post_request(const Req &req, Res &res, Fn &&handler) { try { - jsoncons::json request_body = jsoncons::json::parse(req.body); + jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); handler(request_body); ok_response(res); @@ -37,7 +37,7 @@ void process_post_request(const Req &req, Res &res, Fn &&handler) { template void process_get_request(const Req &req, Res &res, Fn &&handler) { try { - jsoncons::json request_body = jsoncons::json::parse(req.body); + jsoncons::json request_body = jsoncons::json::parse(req.body.empty() ? "{}" : req.body); jsoncons::json reply; handler(request_body, reply); @@ -256,4 +256,4 @@ capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { }else { return; } -} \ No newline at end of file +} From 1d9b52d0674054c8ef0775d951f80f9223b1120a Mon Sep 17 00:00:00 2001 From: Marco Edoardo Santimaria Date: Fri, 30 Jan 2026 21:12:45 +0000 Subject: [PATCH 19/19] fixes --- README.md | 386 +------------------------------------------------ src/webapi.cpp | 2 +- 2 files changed, 4 insertions(+), 384 deletions(-) diff --git a/README.md b/README.md index 5e6bb00..1bfabc2 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,9 @@ Serializer.dump(engine, "my_workflow", "my_workflow.json") ## CapioCL Web API Documentation This section describes the REST-style Web API exposed by the CapioCL Web Server. -The server provides HTTP endpoints for configuring and querying the CapioCL engine at runtime. +The server provides HTTP endpoints for configuring and querying the CapioCL engine at runtime. +Within the `bruno_webapi_tests` you can find several tests and examples on how to perform +requests to the API webserver using [bruno](https://www.usebruno.com). All endpoints communicate using JSON over HTTP. To enable the webserver, users needs to explicitly start it with: @@ -216,388 +218,6 @@ authentication services are currently available, and as such, users should put particular care when allowing connections from external endpoints. ---- - -### Content Type - -All requests and responses use: - -``` -Content-Type: application/json -``` - -### Error Handling - -If a request body is invalid or missing required fields, the server responds with: - -```json -{ - "status": "error", - "what": "Invalid request BODY data:
" -} -``` - -HTTP status code: **400** - -### Success Responses - -For POST endpoints: - -```json -{ - "status": "OK" -} -``` - -For GET endpoints, a JSON object with the requested data is returned. - -HTTP status code: **200** - ---- - -### POST /producer - -Registers a producer for a given path. - -**Request Body** - -```json -{ - "path": "string", - "producer": "string" -} -``` - -**Example** - -```bash -curl -X POST http://localhost:PORT/producer \ - -H "Content-Type: application/json" \ - -d '{"path":"src/file.cpp","producer":"compile"}' -``` - ---- - -### GET /producer - -Returns all producers associated with a path. - -**Request Body** - -```json -{ - "path": "string" -} -``` - -**Response** - -```json -{ - "producers": [ - "compile", - "link" - ] -} -``` - ---- - -### POST /consumer - -Registers a consumer for a given path. - -**Request Body** - -```json -{ - "path": "string", - "consumer": "string" -} -``` - ---- - -### GET /consumer - -Returns all consumers associated with a path. - -**Response** - -```json -{ - "consumers": [ - "test", - "package" - ] -} -``` - ---- - -### POST /dependency - -Adds a file dependency for a path. - -**Request Body** - -```json -{ - "path": "string", - "dependency": "relative/or/absolute/path" -} -``` - ---- - -### GET /dependency - -Returns file dependencies that trigger commits. - -**Response** - -```json -{ - "dependencies": [ - "file1.cpp", - "file2.hpp" - ] -} -``` - ---- - -### POST /commit - -Sets the commit rule for a path. - -**Request Body** - -```json -{ - "path": "string", - "commit": "rule-expression" -} -``` - ---- - -### GET /commit - -Gets the commit rule for a path. - -**Response** - -```json -{ - "commit": "rule-expression" -} -``` - ---- - -### POST /commit/file-count - -Sets the file count required to commit a directory. - -**Request Body** - -```json -{ - "path": "string", - "count": 5 -} -``` - ---- - -### GET /commit/file-count - -Returns the directory file count. - -**Response** - -```json -{ - "count": 5 -} -``` - ---- - -### POST /commit/close-count - -Sets the close count required to commit. - -**Request Body** - -```json -{ - "path": "string", - "count": 2 -} -``` - ---- - -### GET /commit/close-count - -Returns the commit close count. - -**Response** - -```json -{ - "count": 2 -} -``` - ---- - -### POST /fire - -Sets the fire rule for a path. - -**Request Body** - -```json -{ - "path": "string", - "fire": "rule-expression" -} -``` - ---- - -### GET /fire - -Returns the fire rule. - -**Response** - -```json -{ - "fire": "rule-expression" -} -``` - ---- - -### POST /permanent - -Marks a path as permanent or temporary. - -**Request Body** - -```json -{ - "path": "string", - "permanent": true -} -``` - ---- - -### GET /permanent - -Returns permanent status. - -**Response** - -```json -{ - "permanent": true -} -``` - ---- - -### POST /exclude - -Marks a path as excluded or included. - -**Request Body** - -```json -{ - "path": "string", - "exclude": true -} -``` - ---- - -### GET /exclude - -Returns exclusion status. - -**Response** - -```json -{ - "exclude": false -} -``` - ---- - -### POST /directory - -Marks a path as a directory or file. - -**Request Body** - -```json -{ - "path": "string", - "directory": true -} -``` - ---- - -### GET /directory - -Returns directory status. - -**Response** - -```json -{ - "directory": true -} -``` - ---- - -### POST /workflow - -Sets the workflow name. - -**Request Body** - -```json -{ - "name": "build-and-test" -} -``` - ---- - -### GET /workflow - -Returns the workflow name. - -**Response** - -```json -{ - "name": "build-and-test" -} -``` - ---- - ## Notes - All GET endpoints expect a JSON body containing the targeted file path. diff --git a/src/webapi.cpp b/src/webapi.cpp index 6439f90..5542911 100644 --- a/src/webapi.cpp +++ b/src/webapi.cpp @@ -253,7 +253,7 @@ capiocl::webapi::CapioClWebApiServer::~CapioClWebApiServer() { client.Get("/terminate"); if (_webApiThread.joinable()) { _webApiThread.join(); - }else { + } else { return; } }