From 04e0802ef2f41329853c0a935a8ef6d1044fc75b Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Sat, 30 May 2026 20:34:52 +1000 Subject: [PATCH] HTTP boundry --- docs/CURRENT_SYSTEM_ARCHITECTURE.md | 9 +- docs/FORKING_RENDER_CADENCE_BASE.md | 6 +- src/README.md | 5 +- src/app/RenderCadenceApp.h | 21 ++- src/app/RenderCadenceHttpRoutes.cpp | 120 ++++++++++++++++++ src/app/RenderCadenceHttpRoutes.h | 25 ++++ src/control/http/HttpControlServer.cpp | 6 +- src/control/http/HttpControlServer.h | 57 ++++----- src/control/http/HttpControlServerRoutes.cpp | 93 -------------- .../http/HttpControlServerWebSocket.cpp | 2 +- tests/CMakeLists.txt | 1 + ...adenceCompositorHttpControlServerTests.cpp | 65 ++++++---- video-io-3rdParty | 2 +- 13 files changed, 247 insertions(+), 165 deletions(-) create mode 100644 src/app/RenderCadenceHttpRoutes.cpp create mode 100644 src/app/RenderCadenceHttpRoutes.h diff --git a/docs/CURRENT_SYSTEM_ARCHITECTURE.md b/docs/CURRENT_SYSTEM_ARCHITECTURE.md index abbc519..5e9370d 100644 --- a/docs/CURRENT_SYSTEM_ARCHITECTURE.md +++ b/docs/CURRENT_SYSTEM_ARCHITECTURE.md @@ -34,7 +34,8 @@ Primary source areas: - `src/runtime/shader`: background Slang build bridge and prepared shader artifact types - `src/runtime/state`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence - `src/runtime/text`: MSDF/MTSDF font atlas build and CPU-side prepared text texture composition -- `src/control`: HTTP routing, command parsing, OpenAPI state JSON +- `src/control`: command parsing, HTTP/WebSocket transport helpers, OpenAPI state JSON +- `src/app/RenderCadenceHttpRoutes.*`: this app's current HTTP endpoint map - `src/preview`: optional non-consuming preview window - `src/telemetry` and `src/logging`: runtime observation and logging @@ -149,7 +150,7 @@ Screenshot routes are present in the UI/OpenAPI surface but are not implemented ## Control Surface -The HTTP server runs on its own thread. It serves: +The HTTP server runs on its own thread. `HttpControlServer` owns socket lifetime, HTTP parsing, static asset helpers, OpenAPI/Swagger helper serving, and WebSocket state transport. `RenderCadenceHttpRoutes` owns this app's current endpoint map: - UI assets - OpenAPI/Swagger docs @@ -167,6 +168,8 @@ Known but not implemented in the current native command path: Unsupported routes return an action response with `ok: false`. +Forks can reuse the HTTP/WebSocket shell without keeping these endpoints by installing a different route callback. + ## Tests Native tests cover the main non-GL contracts: @@ -177,7 +180,7 @@ Native tests cover the main non-GL contracts: - supported shader catalog - runtime layer restore/reload behavior - runtime-state persistence writer -- HTTP command parsing +- HTTP transport and app-route dispatch - frame exchange and input mailbox behavior - video format and scheduling helpers diff --git a/docs/FORKING_RENDER_CADENCE_BASE.md b/docs/FORKING_RENDER_CADENCE_BASE.md index 86ed89c..45b9712 100644 --- a/docs/FORKING_RENDER_CADENCE_BASE.md +++ b/docs/FORKING_RENDER_CADENCE_BASE.md @@ -21,7 +21,8 @@ These parts are the useful base for the fork: - `src/render/readback`: BGRA8/UYVY8 PBO readback and completed-frame publication. - `src/platform`: hidden GL window/context support. - `src/app`: startup, config, video backend factory, runtime layer orchestration, preview, telemetry, and HTTP server hookup. -- `src/control`, `src/telemetry`, `src/logging`, and `ui`: useful if the new repo still wants a local control surface. +- `src/control/http`, `src/telemetry`, `src/logging`, and `ui`: useful if the new repo still wants a local control surface. +- `src/app/RenderCadenceHttpRoutes.*`: useful only if the new repo keeps this app's current `/api/...` control surface. ## Replace Or Rework @@ -32,10 +33,13 @@ These are most likely to change when the fork renders something other than shade - `src/shader`: shader package manifest parsing and Slang wrapper generation, unless the new renderer keeps the same shader package contract. - `shaders/`: bundled shader package library. - `runtime/templates/shader_wrapper.slang.in`: only needed for the current Slang package pipeline. +- `src/app/RenderCadenceHttpRoutes.*`: replace this with a fork-owned route module if the new renderer has different controls, while keeping `src/control/http/HttpControlServer.*` as the socket/static/WebSocket shell. - Shader-specific UI affordances in `ui`, if the new renderer has a different control model. The first fork step is now in place: `RenderThread` preserves the cadence/readback shell and calls a narrow render-content interface behind the draw call. A new repo can swap that implementation without touching video I/O scheduling. +The HTTP control boundary is also split now. `HttpControlServer` owns transport, static-file helpers, OpenAPI helper serving, and WebSocket state transport; `RenderCadenceHttpRoutes` owns this app's REST endpoint map. A fork that wants the same browser/server plumbing can provide its own route callback and leave the Render Cadence-specific endpoints behind. + ## Current Swap Point The render cadence loop now calls `IRenderContent` inside the readback queue call in `src/render/thread/RenderThread.cpp`: diff --git a/src/README.md b/src/README.md index 3c7283d..cdab0a1 100644 --- a/src/README.md +++ b/src/README.md @@ -261,7 +261,7 @@ Current endpoints: - `POST /api/reload`: rescans the shader library, refreshes manifests, and queues recompilation for every catalog-valid layer in the active stack - other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }` -The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, request debounced runtime-state persistence, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly. +The HTTP server runs on its own thread. `control/http/HttpControlServer` owns socket lifetime, HTTP parsing, static UI/docs helpers, and WebSocket transport. The Render Cadence endpoint map lives in `app/RenderCadenceHttpRoutes`, which samples/copies telemetry through callbacks and translates POST bodies into runtime control commands. A fork can keep the HTTP/WebSocket shell and install a different route callback without inheriting this app's `/api/...` surface. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, request debounced runtime-state persistence, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly. ## Optional DeckLink Output @@ -463,7 +463,8 @@ This app keeps the same core behavior but splits it into modules that can grow: - `runtime/state/`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence - `runtime/text/`: font atlas build and prepared text texture composition - `control/`: control action results and runtime-state JSON presentation -- `control/http/`: local HTTP API, static UI serving, OpenAPI serving, and WebSocket updates +- `control/http/`: local HTTP transport, static UI/OpenAPI serving helpers, and WebSocket updates +- `app/RenderCadenceHttpRoutes`: this app's `/api/...` endpoint map behind the reusable HTTP server route callback - `json/`: compact JSON serialization helpers - `video/`: DeckLink output wrapper and scheduling thread - `telemetry/`: cadence telemetry diff --git a/src/app/RenderCadenceApp.h b/src/app/RenderCadenceApp.h index 1a129ea..c887668 100644 --- a/src/app/RenderCadenceApp.h +++ b/src/app/RenderCadenceApp.h @@ -4,6 +4,7 @@ #include "AppConfigJson.h" #include "AppConfigProvider.h" #include "AppRestart.h" +#include "RenderCadenceHttpRoutes.h" #include "RuntimeLayerController.h" #include "../logging/Logger.h" #include "../control/RuntimeStateJson.h" @@ -246,23 +247,23 @@ private: void StartHttpServer() { - HttpControlServerCallbacks callbacks; - callbacks.getStateJson = [this]() { + RenderCadenceHttpRouteCallbacks routeCallbacks; + routeCallbacks.getStateJson = [this]() { return BuildStateJson(); }; - callbacks.getConfigJson = [this]() { + routeCallbacks.getConfigJson = [this]() { return BuildConfigJson(); }; - callbacks.getNdiSourcesJson = [this]() { + routeCallbacks.getNdiSourcesJson = [this]() { return BuildNdiSourcesJson(); }; - callbacks.addLayer = [this](const std::string& body) { + routeCallbacks.addLayer = [this](const std::string& body) { return mRuntimeLayers.HandleAddLayer(body); }; - callbacks.removeLayer = [this](const std::string& body) { + routeCallbacks.removeLayer = [this](const std::string& body) { return mRuntimeLayers.HandleRemoveLayer(body); }; - callbacks.executePost = [this](const std::string& path, const std::string& body) { + routeCallbacks.executePost = [this](const std::string& path, const std::string& body) { if (path == "/api/config/save") return HandleConfigSave(body); if (path == "/api/app/restart") @@ -275,6 +276,12 @@ private: return mRuntimeLayers.HandleControlCommand(command); }; + HttpControlServerCallbacks callbacks; + callbacks.getWebSocketStateJson = routeCallbacks.getStateJson; + callbacks.routeRequest = [this, routeCallbacks](const HttpRequest& request) { + return RouteRenderCadenceHttpRequest(request, mHttpServer, routeCallbacks); + }; + std::string error; if (!mHttpServer.Start( FindRepoPath("ui/dist"), diff --git a/src/app/RenderCadenceHttpRoutes.cpp b/src/app/RenderCadenceHttpRoutes.cpp new file mode 100644 index 0000000..853fa94 --- /dev/null +++ b/src/app/RenderCadenceHttpRoutes.cpp @@ -0,0 +1,120 @@ +#include "RenderCadenceHttpRoutes.h" + +#include "../json/JsonWriter.h" + +namespace RenderCadenceCompositor +{ +namespace +{ +bool IsKnownPostEndpoint(const std::string& path) +{ + return path == "/api/layers/add" + || path == "/api/layers/remove" + || path == "/api/layers/move" + || path == "/api/layers/reorder" + || path == "/api/layers/set-bypass" + || path == "/api/layers/set-shader" + || path == "/api/layers/update-parameter" + || path == "/api/layers/reset-parameters" + || path == "/api/stack-presets/save" + || path == "/api/stack-presets/load" + || path == "/api/config/save" + || path == "/api/app/restart" + || path == "/api/reload" + || path == "/api/screenshot"; +} + +std::string ActionResponse(bool ok, const std::string& error = std::string()) +{ + JsonWriter writer; + writer.BeginObject(); + writer.KeyBool("ok", ok); + if (!error.empty()) + writer.KeyString("error", error); + writer.EndObject(); + return writer.StringValue(); +} + +HttpResponse ServeRenderCadenceGet( + const HttpRequest& request, + const HttpControlServer& server, + const RenderCadenceHttpRouteCallbacks& callbacks) +{ + if (request.path == "/api/state") + return HttpControlServer::JsonResponse("200 OK", callbacks.getStateJson ? callbacks.getStateJson() : "{}"); + if (request.path == "/api/config") + return HttpControlServer::JsonResponse("200 OK", callbacks.getConfigJson ? callbacks.getConfigJson() : "{}"); + if (request.path == "/api/ndi/sources") + return HttpControlServer::JsonResponse( + "200 OK", + callbacks.getNdiSourcesJson + ? callbacks.getNdiSourcesJson() + : "{\"ok\":false,\"sources\":[],\"error\":\"NDI source discovery is not available.\"}"); + if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml") + return server.ServeOpenApiSpec(); + if (request.path == "/docs" || request.path == "/docs/") + return server.ServeSwaggerDocs(); + if (request.path == "/" || request.path == "/index.html") + return server.ServeUiAsset("index.html"); + if (request.path.rfind("/assets/", 0) == 0) + return server.ServeUiAsset(request.path.substr(1)); + if (request.path.size() > 1) + { + const HttpResponse asset = server.ServeUiAsset(request.path.substr(1)); + if (asset.status != "404 Not Found") + return asset; + } + return server.ServeUiAsset("index.html"); +} + +HttpResponse ServeRenderCadencePost( + const HttpRequest& request, + const RenderCadenceHttpRouteCallbacks& callbacks) +{ + if (!IsKnownPostEndpoint(request.path)) + return HttpControlServer::TextResponse("404 Not Found", "Not Found"); + + if (callbacks.executePost) + { + const ControlActionResult result = callbacks.executePost(request.path, request.body); + return HttpControlServer::JsonResponse( + result.ok ? "200 OK" : "400 Bad Request", + ActionResponse(result.ok, result.error)); + } + + if (request.path == "/api/layers/add" && callbacks.addLayer) + { + const ControlActionResult result = callbacks.addLayer(request.body); + return HttpControlServer::JsonResponse( + result.ok ? "200 OK" : "400 Bad Request", + ActionResponse(result.ok, result.error)); + } + + if (request.path == "/api/layers/remove" && callbacks.removeLayer) + { + const ControlActionResult result = callbacks.removeLayer(request.body); + return HttpControlServer::JsonResponse( + result.ok ? "200 OK" : "400 Bad Request", + ActionResponse(result.ok, result.error)); + } + + return { + "400 Bad Request", + "application/json", + ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.") + }; +} +} + +HttpResponse RouteRenderCadenceHttpRequest( + const HttpRequest& request, + const HttpControlServer& server, + const RenderCadenceHttpRouteCallbacks& callbacks) +{ + if (request.method == "GET") + return ServeRenderCadenceGet(request, server, callbacks); + if (request.method == "POST") + return ServeRenderCadencePost(request, callbacks); + return HttpControlServer::TextResponse("404 Not Found", "Not Found"); +} +} diff --git a/src/app/RenderCadenceHttpRoutes.h b/src/app/RenderCadenceHttpRoutes.h new file mode 100644 index 0000000..01427ef --- /dev/null +++ b/src/app/RenderCadenceHttpRoutes.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../control/ControlActionResult.h" +#include "../control/http/HttpControlServer.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +struct RenderCadenceHttpRouteCallbacks +{ + std::function getStateJson; + std::function getConfigJson; + std::function getNdiSourcesJson; + std::function addLayer; + std::function removeLayer; + std::function executePost; +}; + +HttpResponse RouteRenderCadenceHttpRequest( + const HttpRequest& request, + const HttpControlServer& server, + const RenderCadenceHttpRouteCallbacks& callbacks); +} diff --git a/src/control/http/HttpControlServer.cpp b/src/control/http/HttpControlServer.cpp index 79b69ef..3e8c223 100644 --- a/src/control/http/HttpControlServer.cpp +++ b/src/control/http/HttpControlServer.cpp @@ -231,12 +231,10 @@ bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& re HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpRequest& request) const { - if (request.method == "GET") - return ServeGet(request); - if (request.method == "POST") - return ServePost(request); if (request.method == "OPTIONS") return TextResponse("204 No Content", std::string()); + if (mCallbacks.routeRequest) + return mCallbacks.routeRequest(request); return TextResponse("404 Not Found", "Not Found"); } diff --git a/src/control/http/HttpControlServer.h b/src/control/http/HttpControlServer.h index 5f76fb6..e6fd1be 100644 --- a/src/control/http/HttpControlServer.h +++ b/src/control/http/HttpControlServer.h @@ -1,7 +1,5 @@ #pragma once -#include "ControlActionResult.h" - #include #include @@ -23,14 +21,25 @@ struct HttpControlServerConfig std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10); }; +struct HttpRequest +{ + std::string method; + std::string path; + std::map headers; + std::string body; +}; + +struct HttpResponse +{ + std::string status; + std::string contentType; + std::string body; +}; + struct HttpControlServerCallbacks { - std::function getStateJson; - std::function getConfigJson; - std::function getNdiSourcesJson; - std::function addLayer; - std::function removeLayer; - std::function executePost; + std::function routeRequest; + std::function getWebSocketStateJson; }; class UniqueSocket @@ -57,20 +66,8 @@ private: class HttpControlServer { public: - struct HttpRequest - { - std::string method; - std::string path; - std::map headers; - std::string body; - }; - - struct HttpResponse - { - std::string status; - std::string contentType; - std::string body; - }; + using HttpRequest = RenderCadenceCompositor::HttpRequest; + using HttpResponse = RenderCadenceCompositor::HttpResponse; HttpControlServer() = default; ~HttpControlServer(); @@ -95,6 +92,13 @@ public: static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request); static std::string WebSocketAcceptKey(const std::string& clientKey); + HttpResponse ServeOpenApiSpec() const; + HttpResponse ServeSwaggerDocs() const; + HttpResponse ServeUiAsset(const std::string& relativePath) const; + + static HttpResponse JsonResponse(const std::string& status, const std::string& body); + static HttpResponse TextResponse(const std::string& status, const std::string& body); + static HttpResponse HtmlResponse(const std::string& status, const std::string& body); private: void ThreadMain(); @@ -105,17 +109,8 @@ private: void JoinFinishedClientThreads(); bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const; HttpResponse RouteRequest(const HttpRequest& request) const; - HttpResponse ServeGet(const HttpRequest& request) const; - HttpResponse ServePost(const HttpRequest& request) const; - HttpResponse ServeOpenApiSpec() const; - HttpResponse ServeSwaggerDocs() const; - HttpResponse ServeUiAsset(const std::string& relativePath) const; std::string LoadTextFile(const std::filesystem::path& path) const; - static HttpResponse JsonResponse(const std::string& status, const std::string& body); - static HttpResponse TextResponse(const std::string& status, const std::string& body); - static HttpResponse HtmlResponse(const std::string& status, const std::string& body); - static std::string ActionResponse(bool ok, const std::string& error = std::string()); static bool SendWebSocketText(SOCKET clientSocket, const std::string& text); static std::string GuessContentType(const std::filesystem::path& path); static bool IsSafeRelativePath(const std::filesystem::path& path); diff --git a/src/control/http/HttpControlServerRoutes.cpp b/src/control/http/HttpControlServerRoutes.cpp index a3a66bf..ecc1ab1 100644 --- a/src/control/http/HttpControlServerRoutes.cpp +++ b/src/control/http/HttpControlServerRoutes.cpp @@ -1,7 +1,5 @@ #include "HttpControlServer.h" -#include "../json/JsonWriter.h" - #include #include #include @@ -9,86 +7,6 @@ namespace RenderCadenceCompositor { -namespace -{ -bool IsKnownPostEndpoint(const std::string& path) -{ - return path == "/api/layers/add" - || path == "/api/layers/remove" - || path == "/api/layers/move" - || path == "/api/layers/reorder" - || path == "/api/layers/set-bypass" - || path == "/api/layers/set-shader" - || path == "/api/layers/update-parameter" - || path == "/api/layers/reset-parameters" - || path == "/api/stack-presets/save" - || path == "/api/stack-presets/load" - || path == "/api/config/save" - || path == "/api/app/restart" - || path == "/api/reload" - || path == "/api/screenshot"; -} -} - -HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const -{ - if (request.path == "/api/state") - return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}"); - if (request.path == "/api/config") - return JsonResponse("200 OK", mCallbacks.getConfigJson ? mCallbacks.getConfigJson() : "{}"); - if (request.path == "/api/ndi/sources") - return JsonResponse( - "200 OK", - mCallbacks.getNdiSourcesJson - ? mCallbacks.getNdiSourcesJson() - : "{\"ok\":false,\"sources\":[],\"error\":\"NDI source discovery is not available.\"}"); - if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml") - return ServeOpenApiSpec(); - if (request.path == "/docs" || request.path == "/docs/") - return ServeSwaggerDocs(); - if (request.path == "/" || request.path == "/index.html") - return ServeUiAsset("index.html"); - if (request.path.rfind("/assets/", 0) == 0) - return ServeUiAsset(request.path.substr(1)); - if (request.path.size() > 1) - { - const HttpResponse asset = ServeUiAsset(request.path.substr(1)); - if (asset.status != "404 Not Found") - return asset; - } - return ServeUiAsset("index.html"); -} - -HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const -{ - if (!IsKnownPostEndpoint(request.path)) - return TextResponse("404 Not Found", "Not Found"); - - if (mCallbacks.executePost) - { - const ControlActionResult result = mCallbacks.executePost(request.path, request.body); - return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); - } - - if (request.path == "/api/layers/add" && mCallbacks.addLayer) - { - const ControlActionResult result = mCallbacks.addLayer(request.body); - return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); - } - - if (request.path == "/api/layers/remove" && mCallbacks.removeLayer) - { - const ControlActionResult result = mCallbacks.removeLayer(request.body); - return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); - } - - return { - "400 Bad Request", - "application/json", - ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.") - }; -} - HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const { const std::filesystem::path path = mDocsRoot / "openapi.yaml"; @@ -153,17 +71,6 @@ HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::strin return { status, "text/html", body }; } -std::string HttpControlServer::ActionResponse(bool ok, const std::string& error) -{ - JsonWriter writer; - writer.BeginObject(); - writer.KeyBool("ok", ok); - if (!error.empty()) - writer.KeyString("error", error); - writer.EndObject(); - return writer.StringValue(); -} - std::string HttpControlServer::GuessContentType(const std::filesystem::path& path) { const std::string extension = ToLower(path.extension().string()); diff --git a/src/control/http/HttpControlServerWebSocket.cpp b/src/control/http/HttpControlServerWebSocket.cpp index aa94df7..0b0b980 100644 --- a/src/control/http/HttpControlServerWebSocket.cpp +++ b/src/control/http/HttpControlServerWebSocket.cpp @@ -155,7 +155,7 @@ void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket) std::string previousState; while (mRunning.load(std::memory_order_acquire)) { - const std::string state = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}"; + const std::string state = mCallbacks.getWebSocketStateJson ? mCallbacks.getWebSocketStateJson() : "{}"; if (state != previousState) { if (!SendWebSocketText(clientSocket.get(), state)) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 90baebb..80b85fb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,6 +86,7 @@ add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests ) add_video_shader_test(RenderCadenceCompositorHttpControlServerTests + "${SRC_DIR}/app/RenderCadenceHttpRoutes.cpp" "${SRC_DIR}/control/RuntimeControlCommand.cpp" "${SRC_DIR}/control/http/HttpControlServer.cpp" "${SRC_DIR}/control/http/HttpControlServerRoutes.cpp" diff --git a/tests/RenderCadenceCompositorHttpControlServerTests.cpp b/tests/RenderCadenceCompositorHttpControlServerTests.cpp index 0b8e81c..15e41fa 100644 --- a/tests/RenderCadenceCompositorHttpControlServerTests.cpp +++ b/tests/RenderCadenceCompositorHttpControlServerTests.cpp @@ -1,4 +1,5 @@ #include "HttpControlServer.h" +#include "RenderCadenceHttpRoutes.h" #include "RuntimeControlCommand.h" #include @@ -45,20 +46,41 @@ void TestParsesHttpRequest() ExpectEquals(request.headers["host"], "127.0.0.1", "headers are lower-cased and trimmed"); } -void TestStateEndpointUsesCallback() +void TestServerDelegatesToRouteCallback() { using namespace RenderCadenceCompositor; HttpControlServer server; HttpControlServerCallbacks callbacks; - callbacks.getStateJson = []() { return std::string("{\"ok\":true}"); }; + callbacks.routeRequest = [](const HttpRequest& request) { + ExpectEquals(request.path, "/custom", "generic route callback receives request path"); + return HttpControlServer::JsonResponse("200 OK", "{\"custom\":true}"); + }; server.SetCallbacksForTest(callbacks); + HttpControlServer::HttpRequest request; + request.method = "GET"; + request.path = "/custom"; + + const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + ExpectEquals(response.status, "200 OK", "server delegates HTTP request to route callback"); + ExpectEquals(response.contentType, "application/json", "route callback controls content type"); + ExpectEquals(response.body, "{\"custom\":true}", "route callback controls response body"); +} + +void TestStateEndpointUsesCallback() +{ + using namespace RenderCadenceCompositor; + + HttpControlServer server; + RenderCadenceHttpRouteCallbacks callbacks; + callbacks.getStateJson = []() { return std::string("{\"ok\":true}"); }; + HttpControlServer::HttpRequest request; request.method = "GET"; request.path = "/api/state"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "200 OK", "state endpoint succeeds"); ExpectEquals(response.contentType, "application/json", "state endpoint is JSON"); ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON"); @@ -69,15 +91,14 @@ void TestConfigEndpointUsesCallback() using namespace RenderCadenceCompositor; HttpControlServer server; - HttpControlServerCallbacks callbacks; + RenderCadenceHttpRouteCallbacks callbacks; callbacks.getConfigJson = []() { return std::string("{\"diskLoaded\":true}"); }; - server.SetCallbacksForTest(callbacks); HttpControlServer::HttpRequest request; request.method = "GET"; request.path = "/api/config"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "200 OK", "config endpoint succeeds"); ExpectEquals(response.contentType, "application/json", "config endpoint is JSON"); ExpectEquals(response.body, "{\"diskLoaded\":true}", "config endpoint returns callback JSON"); @@ -88,15 +109,14 @@ void TestNdiSourcesEndpointUsesCallback() using namespace RenderCadenceCompositor; HttpControlServer server; - HttpControlServerCallbacks callbacks; + RenderCadenceHttpRouteCallbacks callbacks; callbacks.getNdiSourcesJson = []() { return std::string("{\"ok\":true,\"sources\":[{\"name\":\"DESKTOP (Camera)\"}]}"); }; - server.SetCallbacksForTest(callbacks); HttpControlServer::HttpRequest request; request.method = "GET"; request.path = "/api/ndi/sources"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "200 OK", "NDI sources endpoint succeeds"); ExpectEquals(response.contentType, "application/json", "NDI sources endpoint is JSON"); Expect(response.body.find("DESKTOP (Camera)") != std::string::npos, "NDI sources endpoint returns callback JSON"); @@ -127,7 +147,8 @@ void TestRootServesUiIndex() request.path = "/"; server.SetRootsForTest(root, std::filesystem::path()); - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + RenderCadenceHttpRouteCallbacks callbacks; + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "200 OK", "root endpoint serves UI index"); ExpectEquals(response.contentType, "text/html", "UI index content type is html"); @@ -146,7 +167,8 @@ void TestKnownPostEndpointReturnsActionError() request.path = "/api/layers/add"; request.body = "{\"shaderId\":\"happy-accident\"}"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + RenderCadenceHttpRouteCallbacks callbacks; + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "400 Bad Request", "unimplemented post returns OpenAPI action error status"); ExpectEquals(response.contentType, "application/json", "unimplemented post returns JSON"); Expect(response.body.find("\"ok\":false") != std::string::npos, "unimplemented post reports ok false"); @@ -158,7 +180,7 @@ void TestLayerPostEndpointsUseCallbacks() using namespace RenderCadenceCompositor; HttpControlServer server; - HttpControlServerCallbacks callbacks; + RenderCadenceHttpRouteCallbacks callbacks; callbacks.addLayer = [](const std::string& body) { Expect(body.find("solid") != std::string::npos, "add callback receives request body"); return ControlActionResult{ true, std::string() }; @@ -167,13 +189,12 @@ void TestLayerPostEndpointsUseCallbacks() Expect(body.find("runtime-layer-1") != std::string::npos, "remove callback receives request body"); return ControlActionResult{ false, "Unknown layer id." }; }; - server.SetCallbacksForTest(callbacks); HttpControlServer::HttpRequest addRequest; addRequest.method = "POST"; addRequest.path = "/api/layers/add"; addRequest.body = "{\"shaderId\":\"solid\"}"; - const HttpControlServer::HttpResponse addResponse = server.RouteRequestForTest(addRequest); + const HttpControlServer::HttpResponse addResponse = RouteRenderCadenceHttpRequest(addRequest, server, callbacks); ExpectEquals(addResponse.status, "200 OK", "add layer callback success returns 200"); Expect(addResponse.body.find("\"ok\":true") != std::string::npos, "add layer callback returns action success"); @@ -181,7 +202,7 @@ void TestLayerPostEndpointsUseCallbacks() removeRequest.method = "POST"; removeRequest.path = "/api/layers/remove"; removeRequest.body = "{\"layerId\":\"runtime-layer-1\"}"; - const HttpControlServer::HttpResponse removeResponse = server.RouteRequestForTest(removeRequest); + const HttpControlServer::HttpResponse removeResponse = RouteRenderCadenceHttpRequest(removeRequest, server, callbacks); ExpectEquals(removeResponse.status, "400 Bad Request", "remove layer callback failure returns 400"); Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic"); } @@ -191,20 +212,19 @@ void TestGenericPostCallbackHandlesControlRoutes() using namespace RenderCadenceCompositor; HttpControlServer server; - HttpControlServerCallbacks callbacks; + RenderCadenceHttpRouteCallbacks callbacks; callbacks.executePost = [](const std::string& path, const std::string& body) { ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path"); Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body"); return ControlActionResult{ true, std::string() }; }; - server.SetCallbacksForTest(callbacks); HttpControlServer::HttpRequest request; request.method = "POST"; request.path = "/api/layers/set-bypass"; request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "200 OK", "generic control callback success returns 200"); Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success"); } @@ -214,20 +234,19 @@ void TestGenericPostCallbackHandlesConfigRoutes() using namespace RenderCadenceCompositor; HttpControlServer server; - HttpControlServerCallbacks callbacks; + RenderCadenceHttpRouteCallbacks callbacks; callbacks.executePost = [](const std::string& path, const std::string& body) { ExpectEquals(path, "/api/config/save", "generic callback receives config route path"); Expect(body.find("runtimeShaderId") != std::string::npos, "generic callback receives config request body"); return ControlActionResult{ true, std::string() }; }; - server.SetCallbacksForTest(callbacks); HttpControlServer::HttpRequest request; request.method = "POST"; request.path = "/api/config/save"; request.body = "{\"runtimeShaderId\":\"solid-color\"}"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "200 OK", "config save callback success returns 200"); Expect(response.body.find("\"ok\":true") != std::string::npos, "config save callback returns action success"); } @@ -251,7 +270,8 @@ void TestUnknownEndpointReturns404() request.method = "GET"; request.path = "/api/nope"; - const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + RenderCadenceHttpRouteCallbacks callbacks; + const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks); ExpectEquals(response.status, "404 Not Found", "unknown endpoint returns 404"); } } @@ -259,6 +279,7 @@ void TestUnknownEndpointReturns404() int main() { TestParsesHttpRequest(); + TestServerDelegatesToRouteCallback(); TestStateEndpointUsesCallback(); TestConfigEndpointUsesCallback(); TestNdiSourcesEndpointUsesCallback(); diff --git a/video-io-3rdParty b/video-io-3rdParty index 6325492..4755423 160000 --- a/video-io-3rdParty +++ b/video-io-3rdParty @@ -1 +1 @@ -Subproject commit 6325492f59dbf4651bcb737b915f2f7f1d138ca3 +Subproject commit 4755423da644cb4bfd6a6d090bc80b1c186ba364