From 213973a323702e2158676d766072b9af29ed21b2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 24 Jul 2025 13:51:16 +0000
Subject: [PATCH] Replace hardcoded SwaggerHandler logic with attribute-based
metadata system
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
---
.../Web/Attributes/OpenApiAttributes.cs | 177 +++++++++++
.../RequestHandlers/DevJsonRequestHandler.cs | 10 +
.../RequestHandlers/DevListRequestHandler.cs | 8 +
.../GetFeedbacksForDeviceRequestHandler.cs | 10 +
.../Web/RequestHandlers/GetRoutesHandler.cs | 7 +
.../RestartProgramRequestHandler.cs | 7 +
.../Web/RequestHandlers/SwaggerHandler.cs | 292 +++++++++++++-----
7 files changed, 434 insertions(+), 77 deletions(-)
create mode 100644 src/PepperDash.Essentials.Core/Web/Attributes/OpenApiAttributes.cs
diff --git a/src/PepperDash.Essentials.Core/Web/Attributes/OpenApiAttributes.cs b/src/PepperDash.Essentials.Core/Web/Attributes/OpenApiAttributes.cs
new file mode 100644
index 00000000..6ac8aa7a
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/Web/Attributes/OpenApiAttributes.cs
@@ -0,0 +1,177 @@
+using System;
+using System.ComponentModel;
+
+namespace PepperDash.Essentials.Core.Web.Attributes
+{
+ ///
+ /// Base class for HTTP method attributes
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public abstract class HttpMethodAttribute : Attribute
+ {
+ public string Method { get; }
+
+ protected HttpMethodAttribute(string method)
+ {
+ Method = method;
+ }
+ }
+
+ ///
+ /// Indicates that a request handler supports HTTP GET operations
+ ///
+ public class HttpGetAttribute : HttpMethodAttribute
+ {
+ public HttpGetAttribute() : base("GET") { }
+ }
+
+ ///
+ /// Indicates that a request handler supports HTTP POST operations
+ ///
+ public class HttpPostAttribute : HttpMethodAttribute
+ {
+ public HttpPostAttribute() : base("POST") { }
+ }
+
+ ///
+ /// Provides OpenAPI operation metadata for a request handler
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public class OpenApiOperationAttribute : Attribute
+ {
+ ///
+ /// A brief summary of what the operation does
+ ///
+ public string Summary { get; set; }
+
+ ///
+ /// A verbose explanation of the operation behavior
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// Unique string used to identify the operation
+ ///
+ public string OperationId { get; set; }
+
+ ///
+ /// A list of tags for API documentation control
+ ///
+ public string[] Tags { get; set; }
+
+ public OpenApiOperationAttribute()
+ {
+ }
+ }
+
+ ///
+ /// Describes a response from an API operation
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public class OpenApiResponseAttribute : Attribute
+ {
+ ///
+ /// The HTTP status code
+ ///
+ public int StatusCode { get; }
+
+ ///
+ /// A short description of the response
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// The content type of the response
+ ///
+ public string ContentType { get; set; } = "application/json";
+
+ ///
+ /// The type that represents the response schema
+ ///
+ public Type Type { get; set; }
+
+ public OpenApiResponseAttribute(int statusCode)
+ {
+ StatusCode = statusCode;
+ }
+ }
+
+ ///
+ /// Indicates that an operation requires a request body
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public class OpenApiRequestBodyAttribute : Attribute
+ {
+ ///
+ /// Determines if the request body is required
+ ///
+ public bool Required { get; set; } = true;
+
+ ///
+ /// The content type of the request body
+ ///
+ public string ContentType { get; set; } = "application/json";
+
+ ///
+ /// The type that represents the request body schema
+ ///
+ public Type Type { get; set; }
+
+ ///
+ /// Description of the request body
+ ///
+ public string Description { get; set; }
+
+ public OpenApiRequestBodyAttribute()
+ {
+ }
+ }
+
+ ///
+ /// Describes a parameter for the operation
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+ public class OpenApiParameterAttribute : Attribute
+ {
+ ///
+ /// The name of the parameter
+ ///
+ public string Name { get; }
+
+ ///
+ /// The location of the parameter
+ ///
+ public ParameterLocation In { get; set; } = ParameterLocation.Path;
+
+ ///
+ /// Determines whether this parameter is mandatory
+ ///
+ public bool Required { get; set; } = true;
+
+ ///
+ /// A brief description of the parameter
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// The type of the parameter
+ ///
+ public Type Type { get; set; } = typeof(string);
+
+ public OpenApiParameterAttribute(string name)
+ {
+ Name = name;
+ }
+ }
+
+ ///
+ /// The location of the parameter
+ ///
+ public enum ParameterLocation
+ {
+ Query,
+ Header,
+ Path,
+ Cookie
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs
index d14ffb83..e0ee7900 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevJsonRequestHandler.cs
@@ -3,10 +3,20 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
+using PepperDash.Essentials.Core.Web.Attributes;
using Serilog.Events;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
+ [HttpPost]
+ [OpenApiOperation(
+ Summary = "DevJson",
+ Description = "Send a command to a specific device",
+ OperationId = "sendDeviceCommand")]
+ [OpenApiParameter("deviceKey", Description = "The key of the device to send the command to")]
+ [OpenApiRequestBody(Description = "Device command data")]
+ [OpenApiResponse(200, Description = "Command executed successfully")]
+ [OpenApiResponse(400, Description = "Bad Request")]
public class DevJsonRequestHandler : WebApiBaseRequestHandler
{
///
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs
index a83685fb..9a343739 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DevListRequestHandler.cs
@@ -2,9 +2,17 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
+using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
+ [HttpGet]
+ [OpenApiOperation(
+ Summary = "DevList",
+ Description = "Retrieve a list of all devices in the system",
+ OperationId = "getDevices")]
+ [OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
+ [OpenApiResponse(404, Description = "Not Found")]
public class DevListRequestHandler : WebApiBaseRequestHandler
{
///
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs
index 5dd5495c..aef7c2a6 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs
@@ -2,9 +2,19 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
+using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
+ [HttpGet]
+ [OpenApiOperation(
+ Summary = "GetFeedbacksForDeviceKey",
+ Description = "Get feedback values from a specific device",
+ OperationId = "getDeviceFeedbacks")]
+ [OpenApiParameter("deviceKey", Description = "The key of the device to get feedbacks from")]
+ [OpenApiResponse(200, Description = "Device feedback values")]
+ [OpenApiResponse(400, Description = "Bad Request")]
+ [OpenApiResponse(404, Description = "Device not found")]
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
{
///
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs
index 2ba9cb33..9934d178 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutesHandler.cs
@@ -2,9 +2,16 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
+using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
+ [HttpGet]
+ [OpenApiOperation(
+ Summary = "GetPaths",
+ Description = "Get available API paths and routes",
+ OperationId = "getApiPaths")]
+ [OpenApiResponse(200, Description = "Successful response")]
public class GetRoutesHandler:WebApiBaseRequestHandler
{
private HttpCwsRouteCollection routeCollection;
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs
index 0bb568f6..ed25e466 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/RestartProgramRequestHandler.cs
@@ -3,9 +3,16 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
+using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
+ [HttpPost]
+ [OpenApiOperation(
+ Summary = "Restart Program",
+ Description = "Restart the program",
+ OperationId = "restartProgram")]
+ [OpenApiResponse(200, Description = "Program restart initiated successfully")]
public class RestartProgramRequestHandler : WebApiBaseRequestHandler
{
///
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs
index 6cf11cd8..69255847 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SwaggerHandler.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
+using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
@@ -84,56 +87,155 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
private object GeneratePathItem(HttpCwsRoute route)
{
- // Determine HTTP methods and create appropriate operation objects
+ if (route.RouteHandler == null) return null;
+
+ var handlerType = route.RouteHandler.GetType();
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() ?? "";
+ // Get HTTP method attributes from the handler class
+ var httpMethodAttributes = handlerType.GetCustomAttributes(typeof(HttpMethodAttribute), false)
+ .Cast()
+ .ToList();
- 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"))
+ // If no HTTP method attributes found, fall back to the original logic
+ if (!httpMethodAttributes.Any())
{
- operations["get"] = GenerateOperation(route, "GET");
+ httpMethodAttributes = DetermineHttpMethodsFromRoute(route);
}
- if (routeName.Contains("command") || routeName.Contains("restart") || routeName.Contains("load") ||
- routeName.Contains("debug") || routeName.Contains("disable"))
+ foreach (var methodAttr in httpMethodAttributes)
{
- operations["post"] = GenerateOperation(route, "POST");
+ var operation = GenerateOperation(route, methodAttr.Method, handlerType);
+ if (operation != null)
+ {
+ operations[methodAttr.Method.ToLower()] = operation;
+ }
}
return operations.Count > 0 ? operations : null;
}
- private object GenerateOperation(HttpCwsRoute route, string method)
+ private List DetermineHttpMethodsFromRoute(HttpCwsRoute route)
{
- 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" }
- }
- };
+ var methods = new List();
+ var routeName = route.Name?.ToLower() ?? "";
+ var routeUrl = route.Url?.ToLower() ?? "";
- // Add parameters for path variables
+ // Fallback logic for routes without attributes
+ 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"))
+ {
+ methods.Add(new HttpGetAttribute());
+ }
+
+ if (routeName.Contains("command") || routeName.Contains("restart") || routeName.Contains("load") ||
+ routeName.Contains("debug") || routeName.Contains("disable"))
+ {
+ methods.Add(new HttpPostAttribute());
+ }
+
+ return methods;
+ }
+
+ private object GenerateOperation(HttpCwsRoute route, string method, Type handlerType)
+ {
+ var operation = new Dictionary();
+
+ // Get OpenApiOperation attribute
+ var operationAttr = handlerType.GetCustomAttribute();
+ if (operationAttr != null)
+ {
+ operation["summary"] = operationAttr.Summary ?? route.Name ?? "API Operation";
+ operation["operationId"] = operationAttr.OperationId ?? route.Name?.Replace(" ", "") ?? "operation";
+
+ if (!string.IsNullOrEmpty(operationAttr.Description))
+ {
+ operation["description"] = operationAttr.Description;
+ }
+
+ if (operationAttr.Tags != null && operationAttr.Tags.Length > 0)
+ {
+ operation["tags"] = operationAttr.Tags;
+ }
+ }
+ else
+ {
+ // Fallback to route name
+ operation["summary"] = route.Name ?? "API Operation";
+ operation["operationId"] = route.Name?.Replace(" ", "") ?? "operation";
+
+ // Add fallback description
+ var fallbackDescription = GetFallbackDescription(route);
+ if (!string.IsNullOrEmpty(fallbackDescription))
+ {
+ operation["description"] = fallbackDescription;
+ }
+ }
+
+ // Get response attributes
+ var responses = new Dictionary();
+ var responseAttrs = handlerType.GetCustomAttributes().ToList();
+
+ if (responseAttrs.Any())
+ {
+ foreach (var responseAttr in responseAttrs)
+ {
+ var responseObj = new Dictionary
+ {
+ ["description"] = responseAttr.Description ?? "Response"
+ };
+
+ if (!string.IsNullOrEmpty(responseAttr.ContentType))
+ {
+ responseObj["content"] = new Dictionary
+ {
+ [responseAttr.ContentType] = new { schema = new { type = "object" } }
+ };
+ }
+
+ responses[responseAttr.StatusCode.ToString()] = responseObj;
+ }
+ }
+ else
+ {
+ // Default responses
+ responses["200"] = new
+ {
+ description = "Successful response",
+ content = new Dictionary
+ {
+ ["application/json"] = new { schema = new { type = "object" } }
+ }
+ };
+ responses["400"] = new { description = "Bad Request" };
+ responses["404"] = new { description = "Not Found" };
+ responses["500"] = new { description = "Internal Server Error" };
+ }
+
+ operation["responses"] = responses;
+
+ // Get parameter attributes
+ var parameterAttrs = handlerType.GetCustomAttributes().ToList();
+ var parameters = new List