diff --git a/README.md b/README.md index 7d2f0d9..c417b0f 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,20 @@ The local REST control API is documented as an OpenAPI/Swagger spec: docs/openapi.yaml ``` -Load that file in Swagger UI, Redoc, or another OpenAPI viewer to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket. +When the control server is running, the same spec is also served at: + +```text +http://127.0.0.1:/docs/openapi.yaml +http://127.0.0.1:/openapi.yaml +``` + +A Swagger UI page is available at: + +```text +http://127.0.0.1:/docs +``` + +Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket. ## Shader Packages diff --git a/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp b/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp index 3b5dfe8..9f48515 100644 --- a/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/ControlServer.cpp @@ -54,6 +54,8 @@ std::string GuessContentType(const std::filesystem::path& assetPath) return "text/css"; if (extension == ".json") return "application/json"; + if (extension == ".yaml" || extension == ".yml") + return "application/yaml"; if (extension == ".svg") return "image/svg+xml"; if (extension == ".png") @@ -78,9 +80,10 @@ ControlServer::~ControlServer() Stop(); } -bool ControlServer::Start(const std::filesystem::path& uiRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error) +bool ControlServer::Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error) { mUiRoot = uiRoot; + mDocsRoot = docsRoot; mCallbacks = callbacks; if (!InitializeWinsock(error)) @@ -246,6 +249,12 @@ ControlServer::HttpResponse ControlServer::ServeGetRequest(const HttpRequest& re if (request.path == "/api/state") return { "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}" }; + if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml") + return ServeOpenApiSpec(); + + if (request.path == "/docs" || request.path == "/docs/") + return ServeSwaggerDocs(); + if (request.path.size() > 1) { const HttpResponse assetResponse = ServeUiAsset(request.path.substr(1)); @@ -265,6 +274,35 @@ ControlServer::HttpResponse ControlServer::ServeUiAsset(const std::string& relat : HttpResponse{ "200 OK", contentType, body }; } +ControlServer::HttpResponse ControlServer::ServeOpenApiSpec() const +{ + const std::filesystem::path specPath = mDocsRoot / "openapi.yaml"; + const std::string body = LoadTextFile(specPath); + return body.empty() + ? HttpResponse{ "404 Not Found", "text/plain", "OpenAPI spec not found" } + : HttpResponse{ "200 OK", GuessContentType(specPath), body }; +} + +ControlServer::HttpResponse ControlServer::ServeSwaggerDocs() const +{ + std::ostringstream html; + html << "\n" + << "\n" + << "\n" + << " \n" + << " \n" + << " Video Shader Toys API Docs\n" + << " \n" + << "\n" + << "\n" + << "
\n" + << " \n" + << " \n" + << "\n" + << "\n"; + return { "200 OK", "text/html", html.str() }; +} + ControlServer::HttpResponse ControlServer::HandleApiPost(const HttpRequest& request) { JsonValue root; @@ -449,12 +487,16 @@ std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::str return std::string(); const std::filesystem::path assetPath = mUiRoot / sanitizedPath; - std::ifstream input(assetPath, std::ios::binary); + contentType = GuessContentType(assetPath); + return LoadTextFile(assetPath); +} + +std::string ControlServer::LoadTextFile(const std::filesystem::path& path) const +{ + std::ifstream input(path, std::ios::binary); if (!input) return std::string(); - contentType = GuessContentType(assetPath); - std::ostringstream buffer; buffer << input.rdbuf(); return buffer.str(); diff --git a/apps/LoopThroughWithOpenGLCompositing/ControlServer.h b/apps/LoopThroughWithOpenGLCompositing/ControlServer.h index 0c821bc..1ff092e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/ControlServer.h +++ b/apps/LoopThroughWithOpenGLCompositing/ControlServer.h @@ -37,7 +37,7 @@ public: ControlServer(); ~ControlServer(); - bool Start(const std::filesystem::path& uiRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error); + bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error); void Stop(); void BroadcastState(); @@ -76,11 +76,14 @@ private: HttpResponse RouteHttpRequest(const HttpRequest& request); HttpResponse ServeGetRequest(const HttpRequest& request) const; HttpResponse ServeUiAsset(const std::string& relativePath) const; + HttpResponse ServeOpenApiSpec() const; + HttpResponse ServeSwaggerDocs() const; HttpResponse HandleApiPost(const HttpRequest& request); bool InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError); bool SendWebSocketText(SOCKET clientSocket, const std::string& payload); void BroadcastStateLocked(); std::string LoadUiAsset(const std::string& relativePath, std::string& contentType) const; + std::string LoadTextFile(const std::filesystem::path& path) const; std::string BuildJsonResponse(bool success, const std::string& error = std::string()) const; static std::string Base64Encode(const unsigned char* data, DWORD dataLength); static std::string ComputeWebSocketAcceptKey(const std::string& clientKey); @@ -89,6 +92,7 @@ private: private: std::filesystem::path mUiRoot; + std::filesystem::path mDocsRoot; Callbacks mCallbacks; UniqueSocket mListenSocket; unsigned short mPort; diff --git a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp index 056b0df..9965590 100644 --- a/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/OpenGLComposite.cpp @@ -836,7 +836,7 @@ bool OpenGLComposite::InitOpenGLState() return true; }; - if (!mControlServer->Start(mRuntimeHost->GetUiRoot(), mRuntimeHost->GetServerPort(), callbacks, runtimeError)) + if (!mControlServer->Start(mRuntimeHost->GetUiRoot(), mRuntimeHost->GetDocsRoot(), mRuntimeHost->GetServerPort(), callbacks, runtimeError)) { MessageBoxA(NULL, runtimeError.c_str(), "Local control server failed to start", MB_OK); return false; diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp index 1f3e663..accebf5 100644 --- a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.cpp @@ -1388,6 +1388,7 @@ bool RuntimeHost::ResolvePaths(std::string& error) const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist"; mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui"); + mDocsRoot = mRepoRoot / "docs"; mConfigPath = mRepoRoot / "config" / "runtime-host.json"; mShaderRoot = mRepoRoot / mConfig.shaderLibrary; mRuntimeRoot = mRepoRoot / "runtime"; diff --git a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h index c8a7c94..f220809 100644 --- a/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/RuntimeHost.h @@ -45,6 +45,7 @@ public: const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; } const std::filesystem::path& GetUiRoot() const { return mUiRoot; } + const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; } const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; } unsigned short GetServerPort() const { return mServerPort; } unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; } @@ -118,6 +119,7 @@ private: PersistentState mPersistentState; std::filesystem::path mRepoRoot; std::filesystem::path mUiRoot; + std::filesystem::path mDocsRoot; std::filesystem::path mShaderRoot; std::filesystem::path mRuntimeRoot; std::filesystem::path mPresetRoot; diff --git a/runtime/README.md b/runtime/README.md index 99f108c..8f1de93 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -6,6 +6,11 @@ Tracked files: - `templates/`: source templates used to generate shader runtime code. +Packaged documentation: + +- `../docs/openapi.yaml`: OpenAPI/Swagger spec for the local control API. +- `http://127.0.0.1:/docs`: Swagger UI page served by the native control server. + Generated files: - `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer.