From df61fdfea1de5c9603447d4b0b718f99e3040584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Jul 2025 01:12:20 +0000 Subject: [PATCH] Add SwaggerHandler for OpenAPI 3 documentation endpoint Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com> --- .../Web/EssentialsWebApi.cs | 5 + .../Web/RequestHandlers/SwaggerHandler.cs | 332 ++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index 94dbf5ad..c54163e6 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -210,6 +210,11 @@ namespace PepperDash.Essentials.Core.Web RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath) }); + AddRoute(new HttpCwsRoute("swagger") { + Name = "OpenAPI Documentation", + RouteHandler = new SwaggerHandler(_server.GetRouteCollection(), BasePath) + }); + // If running on an appliance if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance) { diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs new file mode 100644 index 00000000..635e2fc6 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using Crestron.SimplSharp; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers +{ + public class SwaggerHandler : WebApiBaseRequestHandler + { + private HttpCwsRouteCollection routeCollection; + private string basePath; + + public SwaggerHandler(HttpCwsRouteCollection routeCollection, string basePath) + { + this.routeCollection = routeCollection; + this.basePath = basePath; + } + + protected override void HandleGet(HttpCwsContext context) + { + var currentIp = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); + + var hostname = CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); + + var serverUrl = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server + ? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{basePath}" + : $"https://{currentIp}/cws{basePath}"; + + var openApiDoc = GenerateOpenApiDocument(serverUrl); + + var response = JsonConvert.SerializeObject(openApiDoc, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + context.Response.Headers.Add("Content-Type", "application/json"); + context.Response.Write(response, false); + context.Response.End(); + } + + private object GenerateOpenApiDocument(string serverUrl) + { + var paths = new Dictionary(); + + // Add paths based on existing routes + foreach (var route in routeCollection) + { + var pathKey = "/" + route.Url; + var pathItem = GeneratePathItem(route); + if (pathItem != null) + { + paths[pathKey] = pathItem; + } + } + + return new + { + openapi = "3.0.3", + info = new + { + title = "PepperDash Essentials API", + description = "RESTful API for PepperDash Essentials control system", + version = "1.0.0", + contact = new + { + name = "PepperDash Technology", + url = "https://www.pepperdash.com" + } + }, + servers = new[] + { + new { url = serverUrl, description = "Essentials API Server" } + }, + paths = paths, + components = new + { + schemas = GetSchemas() + } + }; + } + + private object GeneratePathItem(HttpCwsRoute route) + { + // Determine HTTP methods and create appropriate operation objects + var operations = new Dictionary(); + + // Based on the route name and common patterns, determine likely HTTP methods + var routeName = route.Name?.ToLower() ?? ""; + var routeUrl = route.Url?.ToLower() ?? ""; + + if (routeName.Contains("get") || routeUrl.Contains("devices") || routeUrl.Contains("config") || + routeUrl.Contains("versions") || routeUrl.Contains("types") || routeUrl.Contains("tielines") || + routeUrl.Contains("apipaths") || routeUrl.Contains("feedbacks") || routeUrl.Contains("properties") || + routeUrl.Contains("methods") || routeUrl.Contains("joinmap") || routeUrl.Contains("routingports")) + { + operations["get"] = GenerateOperation(route, "GET"); + } + + if (routeName.Contains("command") || routeName.Contains("restart") || routeName.Contains("load") || + routeName.Contains("debug") || routeName.Contains("disable")) + { + operations["post"] = GenerateOperation(route, "POST"); + } + + return operations.Count > 0 ? operations : null; + } + + private object GenerateOperation(HttpCwsRoute route, string method) + { + var operation = new Dictionary + { + ["summary"] = route.Name ?? "API Operation", + ["operationId"] = route.Name?.Replace(" ", "") ?? "operation", + ["responses"] = new Dictionary + { + ["200"] = new + { + description = "Successful response", + content = new Dictionary + { + ["application/json"] = new { schema = new { type = "object" } } + } + }, + ["400"] = new { description = "Bad Request" }, + ["404"] = new { description = "Not Found" }, + ["500"] = new { description = "Internal Server Error" } + } + }; + + // Add parameters for path variables + if (route.Url.Contains("{")) + { + var parameters = new List(); + var url = route.Url; + while (url.Contains("{")) + { + var start = url.IndexOf("{"); + var end = url.IndexOf("}", start); + if (end > start) + { + var paramName = url.Substring(start + 1, end - start - 1); + parameters.Add(new + { + name = paramName, + @in = "path", + required = true, + schema = new { type = "string" }, + description = $"The {paramName} parameter" + }); + url = url.Substring(end + 1); + } + else break; + } + if (parameters.Count > 0) + { + operation["parameters"] = parameters; + } + } + + // Add request body for POST operations + if (method == "POST" && route.Name != null && route.Name.Contains("Command")) + { + operation["requestBody"] = new + { + required = true, + content = new Dictionary + { + ["application/json"] = new + { + schema = new { @ref = "#/components/schemas/DeviceCommand" } + } + } + }; + } + + // Add specific descriptions based on route patterns + AddRouteSpecificDescription(operation, route); + + return operation; + } + + private void AddRouteSpecificDescription(Dictionary operation, HttpCwsRoute route) + { + var routeName = route.Name?.ToLower() ?? ""; + var routeUrl = route.Url?.ToLower() ?? ""; + + if (routeUrl.Contains("devices") && !routeUrl.Contains("{")) + { + operation["description"] = "Retrieve a list of all devices in the system"; + } + else if (routeUrl.Contains("versions")) + { + operation["description"] = "Get version information for loaded assemblies"; + } + else if (routeUrl.Contains("config")) + { + operation["description"] = "Retrieve the current system configuration"; + } + else if (routeUrl.Contains("devicecommands")) + { + operation["description"] = "Send a command to a specific device"; + } + else if (routeUrl.Contains("devicefeedbacks")) + { + operation["description"] = "Get feedback values from a specific device"; + } + else if (routeUrl.Contains("deviceproperties")) + { + operation["description"] = "Get properties of a specific device"; + } + else if (routeUrl.Contains("devicemethods")) + { + operation["description"] = "Get available methods for a specific device"; + } + else if (routeUrl.Contains("types")) + { + operation["description"] = routeUrl.Contains("{") ? "Get types filtered by the specified filter" : "Get all available types"; + } + else if (routeUrl.Contains("tielines")) + { + operation["description"] = "Get information about tielines in the system"; + } + else if (routeUrl.Contains("joinmap")) + { + operation["description"] = "Get join map information for bridge or device"; + } + else if (routeUrl.Contains("routingports")) + { + operation["description"] = "Get routing ports for a specific device"; + } + else if (routeUrl.Contains("apipaths")) + { + operation["description"] = "Get available API paths and routes"; + } + else if (routeName.Contains("restart")) + { + operation["description"] = "Restart the program"; + } + else if (routeName.Contains("debug")) + { + operation["description"] = "Debug operation"; + } + } + + private Dictionary GetSchemas() + { + return new Dictionary + { + ["DeviceCommand"] = new + { + type = "object", + properties = new Dictionary + { + ["deviceKey"] = new { type = "string", description = "The key of the device" }, + ["methodName"] = new { type = "string", description = "The method to call on the device" }, + ["params"] = new { type = "array", items = new { type = "object" }, description = "Parameters for the method call" } + }, + required = new[] { "deviceKey", "methodName" } + }, + ["Device"] = new + { + type = "object", + properties = new Dictionary + { + ["key"] = new { type = "string", description = "Device key" }, + ["name"] = new { type = "string", description = "Device name" }, + ["type"] = new { type = "string", description = "Device type" }, + ["isOnline"] = new { type = "boolean", description = "Device online status" } + } + }, + ["Feedback"] = new + { + type = "object", + properties = new Dictionary + { + ["BoolValues"] = new { type = "array", items = new { @ref = "#/components/schemas/BoolFeedback" } }, + ["IntValues"] = new { type = "array", items = new { @ref = "#/components/schemas/IntFeedback" } }, + ["SerialValues"] = new { type = "array", items = new { @ref = "#/components/schemas/StringFeedback" } } + } + }, + ["BoolFeedback"] = new + { + type = "object", + properties = new Dictionary + { + ["FeedbackKey"] = new { type = "string" }, + ["Value"] = new { type = "boolean" } + } + }, + ["IntFeedback"] = new + { + type = "object", + properties = new Dictionary + { + ["FeedbackKey"] = new { type = "string" }, + ["Value"] = new { type = "integer" } + } + }, + ["StringFeedback"] = new + { + type = "object", + properties = new Dictionary + { + ["FeedbackKey"] = new { type = "string" }, + ["Value"] = new { type = "string" } + } + }, + ["ApiRoutes"] = new + { + type = "object", + properties = new Dictionary + { + ["url"] = new { type = "string", description = "Base URL for the API" }, + ["routes"] = new { type = "array", items = new { @ref = "#/components/schemas/Route" } } + } + }, + ["Route"] = new + { + type = "object", + properties = new Dictionary + { + ["name"] = new { type = "string", description = "Route name" }, + ["url"] = new { type = "string", description = "Route URL pattern" } + } + } + }; + } + } +} \ No newline at end of file