diff --git a/include/spore/types/ApiResponse.h b/include/spore/types/ApiResponse.h new file mode 100644 index 0000000..ba87392 --- /dev/null +++ b/include/spore/types/ApiResponse.h @@ -0,0 +1,91 @@ +#pragma once +#include "spore/util/JsonSerializable.h" +#include + +namespace spore { +namespace types { + +/** + * Base class for API responses that can be serialized to JSON + * Handles complete JsonDocument creation and serialization + */ +class ApiResponse { +protected: + mutable JsonDocument doc; + +public: + virtual ~ApiResponse() = default; + + /** + * Get the complete JSON string representation of this response + * @return JSON string ready to send as HTTP response + */ + virtual String toJsonString() const { + String json; + serializeJson(doc, json); + return json; + } + + /** + * Get the JsonDocument for direct manipulation if needed + * @return Reference to the internal JsonDocument + */ + JsonDocument& getDocument() { return doc; } + const JsonDocument& getDocument() const { return doc; } + + /** + * Clear the document and reset for reuse + */ + virtual void clear() { + doc.clear(); + } +}; + +/** + * Base class for API responses that contain a collection of serializable items + */ +template +class CollectionResponse : public ApiResponse { +protected: + String collectionKey; + +public: + explicit CollectionResponse(const String& key) : collectionKey(key) {} + + /** + * Add a serializable item to the collection + * @param item The serializable item to add + */ + void addItem(const util::JsonSerializable& item) { + JsonArray arr = doc[collectionKey].to(); + JsonObject obj = arr.add(); + item.toJson(obj); + } + + /** + * Add multiple items from a container + * @param items Container of serializable items + */ + template + void addItems(const Container& items) { + JsonArray arr = doc[collectionKey].to(); + for (const auto& item : items) { + JsonObject obj = arr.add(); + item.toJson(obj); + } + } + + /** + * Get the current number of items in the collection + * @return Number of items + */ + size_t getItemCount() const { + if (doc[collectionKey].is()) { + return doc[collectionKey].as().size(); + } + return 0; + } +}; + +} // namespace types +} // namespace spore diff --git a/include/spore/types/ClusterResponse.h b/include/spore/types/ClusterResponse.h new file mode 100644 index 0000000..ae5f21b --- /dev/null +++ b/include/spore/types/ClusterResponse.h @@ -0,0 +1,48 @@ +#pragma once +#include "ApiResponse.h" +#include "NodeInfoSerializable.h" +#include + +namespace spore { +namespace types { + +/** + * Response class for cluster members endpoint + * Handles complete JSON document creation for cluster member data + */ +class ClusterMembersResponse : public CollectionResponse { +public: + ClusterMembersResponse() : CollectionResponse("members") {} + + /** + * Add a single node to the response + * @param node The NodeInfo to add + */ + void addNode(const NodeInfo& node) { + NodeInfoSerializable serializable(const_cast(node)); + addItem(serializable); + } + + /** + * Add multiple nodes from a member list + * @param memberList Map of hostname to NodeInfo + */ + void addNodes(const std::map& memberList) { + for (const auto& pair : memberList) { + addNode(pair.second); + } + } + + /** + * Add nodes from a pointer to member list + * @param memberList Pointer to map of hostname to NodeInfo + */ + void addNodes(const std::map* memberList) { + if (memberList) { + addNodes(*memberList); + } + } +}; + +} // namespace types +} // namespace spore diff --git a/include/spore/types/EndpointInfoSerializable.h b/include/spore/types/EndpointInfoSerializable.h new file mode 100644 index 0000000..a09cc7e --- /dev/null +++ b/include/spore/types/EndpointInfoSerializable.h @@ -0,0 +1,76 @@ +#pragma once +#include "ApiTypes.h" +#include "spore/util/JsonSerializable.h" +#include + +namespace spore { +namespace types { + +/** + * Serializable wrapper for EndpointInfo that implements JsonSerializable interface + * Handles conversion between EndpointInfo struct and JSON representation + */ +class EndpointInfoSerializable : public util::JsonSerializable { +private: + const EndpointInfo& endpoint; + +public: + explicit EndpointInfoSerializable(const EndpointInfo& ep) : endpoint(ep) {} + + /** + * Serialize EndpointInfo to JsonObject + */ + void toJson(JsonObject& obj) const override { + obj["uri"] = endpoint.uri; + obj["method"] = endpoint.method; + + // Add parameters if present + if (!endpoint.params.empty()) { + JsonArray paramsArr = obj["params"].to(); + for (const auto& param : endpoint.params) { + JsonObject paramObj = paramsArr.add(); + paramObj["name"] = param.name; + paramObj["location"] = param.location; + paramObj["required"] = param.required; + paramObj["type"] = param.type; + + if (!param.values.empty()) { + JsonArray valuesArr = paramObj["values"].to(); + for (const auto& value : param.values) { + valuesArr.add(value); + } + } + + if (param.defaultValue.length() > 0) { + paramObj["default"] = param.defaultValue; + } + } + } + } + + /** + * Deserialize EndpointInfo from JsonObject + */ + void fromJson(const JsonObject& obj) override { + // Note: This would require modifying the EndpointInfo struct to be mutable + // For now, this is a placeholder as EndpointInfo is typically read-only + // in the context where this serialization is used + } +}; + +/** + * Convenience function to create a JsonArray from a collection of EndpointInfo objects + */ +template +JsonArray endpointInfoToJsonArray(JsonDocument& doc, const Container& endpoints) { + JsonArray arr = doc.to(); + for (const auto& endpoint : endpoints) { + EndpointInfoSerializable serializable(endpoint); + JsonObject obj = arr.add(); + serializable.toJson(obj); + } + return arr; +} + +} // namespace types +} // namespace spore diff --git a/include/spore/types/NodeInfoSerializable.h b/include/spore/types/NodeInfoSerializable.h new file mode 100644 index 0000000..ec94314 --- /dev/null +++ b/include/spore/types/NodeInfoSerializable.h @@ -0,0 +1,141 @@ +#pragma once +#include "NodeInfo.h" +#include "ApiTypes.h" +#include "spore/util/JsonSerializable.h" +#include + +namespace spore { +namespace types { + +/** + * Serializable wrapper for NodeInfo that implements JsonSerializable interface + * Handles conversion between NodeInfo struct and JSON representation + */ +class NodeInfoSerializable : public util::JsonSerializable { +private: + NodeInfo& nodeInfo; + +public: + explicit NodeInfoSerializable(NodeInfo& node) : nodeInfo(node) {} + + /** + * Serialize NodeInfo to JsonObject + * Maps all NodeInfo fields to appropriate JSON structure + */ + void toJson(JsonObject& obj) const override { + obj["hostname"] = nodeInfo.hostname; + obj["ip"] = nodeInfo.ip.toString(); + obj["lastSeen"] = nodeInfo.lastSeen; + obj["latency"] = nodeInfo.latency; + obj["status"] = statusToStr(nodeInfo.status); + + // Serialize resources + JsonObject resources = obj["resources"].to(); + resources["freeHeap"] = nodeInfo.resources.freeHeap; + resources["chipId"] = nodeInfo.resources.chipId; + resources["sdkVersion"] = nodeInfo.resources.sdkVersion; + resources["cpuFreqMHz"] = nodeInfo.resources.cpuFreqMHz; + resources["flashChipSize"] = nodeInfo.resources.flashChipSize; + + // Serialize labels if present + if (!nodeInfo.labels.empty()) { + JsonObject labels = obj["labels"].to(); + for (const auto& kv : nodeInfo.labels) { + labels[kv.first.c_str()] = kv.second; + } + } + + // Serialize endpoints if present + if (!nodeInfo.endpoints.empty()) { + JsonArray endpoints = obj["api"].to(); + for (const auto& endpoint : nodeInfo.endpoints) { + JsonObject endpointObj = endpoints.add(); + endpointObj["uri"] = endpoint.uri; + endpointObj["method"] = endpoint.method; + } + } + } + + /** + * Deserialize NodeInfo from JsonObject + * Populates NodeInfo fields from JSON structure + */ + void fromJson(const JsonObject& obj) override { + nodeInfo.hostname = obj["hostname"].as(); + + // Parse IP address + const char* ipStr = obj["ip"]; + if (ipStr) { + nodeInfo.ip.fromString(ipStr); + } + + nodeInfo.lastSeen = obj["lastSeen"].as(); + nodeInfo.latency = obj["latency"].as(); + + // Parse status + const char* statusStr = obj["status"]; + if (statusStr) { + if (strcmp(statusStr, "ACTIVE") == 0) { + nodeInfo.status = NodeInfo::ACTIVE; + } else if (strcmp(statusStr, "INACTIVE") == 0) { + nodeInfo.status = NodeInfo::INACTIVE; + } else if (strcmp(statusStr, "DEAD") == 0) { + nodeInfo.status = NodeInfo::DEAD; + } + } + + // Parse resources + if (obj["resources"].is()) { + JsonObject resources = obj["resources"].as(); + nodeInfo.resources.freeHeap = resources["freeHeap"].as(); + nodeInfo.resources.chipId = resources["chipId"].as(); + nodeInfo.resources.sdkVersion = resources["sdkVersion"].as(); + nodeInfo.resources.cpuFreqMHz = resources["cpuFreqMHz"].as(); + nodeInfo.resources.flashChipSize = resources["flashChipSize"].as(); + } + + // Parse labels + nodeInfo.labels.clear(); + if (obj["labels"].is()) { + JsonObject labels = obj["labels"].as(); + for (JsonPair kvp : labels) { + nodeInfo.labels[kvp.key().c_str()] = kvp.value().as(); + } + } + + // Parse endpoints + nodeInfo.endpoints.clear(); + if (obj["api"].is()) { + JsonArray endpoints = obj["api"].as(); + for (JsonObject endpointObj : endpoints) { + EndpointInfo endpoint; + endpoint.uri = endpointObj["uri"].as(); + endpoint.method = endpointObj["method"].as(); + endpoint.isLocal = false; + endpoint.serviceName = "remote"; + nodeInfo.endpoints.push_back(std::move(endpoint)); + } + } + } +}; + +/** + * Convenience function to create a JsonArray from a collection of NodeInfo objects + * @param doc The JsonDocument to create the array in + * @param nodes Collection of NodeInfo objects + * @return A JsonArray containing all serialized NodeInfo objects + */ +template +JsonArray nodeInfoToJsonArray(JsonDocument& doc, const Container& nodes) { + JsonArray arr = doc.to(); + for (const auto& pair : nodes) { + const NodeInfo& node = pair.second; + NodeInfoSerializable serializable(const_cast(node)); + JsonObject obj = arr.add(); + serializable.toJson(obj); + } + return arr; +} + +} // namespace types +} // namespace spore diff --git a/include/spore/types/NodeResponse.h b/include/spore/types/NodeResponse.h new file mode 100644 index 0000000..3713a54 --- /dev/null +++ b/include/spore/types/NodeResponse.h @@ -0,0 +1,102 @@ +#pragma once +#include "ApiResponse.h" +#include "EndpointInfoSerializable.h" +#include "NodeInfo.h" +#include + +namespace spore { +namespace types { + +/** + * Response class for node status endpoint + * Handles complete JSON document creation for node status data + */ +class NodeStatusResponse : public ApiResponse { +public: + /** + * Set basic system information + */ + void setSystemInfo() { + doc["freeHeap"] = ESP.getFreeHeap(); + doc["chipId"] = ESP.getChipId(); + doc["sdkVersion"] = ESP.getSdkVersion(); + doc["cpuFreqMHz"] = ESP.getCpuFreqMHz(); + doc["flashChipSize"] = ESP.getFlashChipSize(); + } + + /** + * Add labels to the response + * @param labels Map of label key-value pairs + */ + void addLabels(const std::map& labels) { + if (!labels.empty()) { + JsonObject labelsObj = doc["labels"].to(); + for (const auto& kv : labels) { + labelsObj[kv.first.c_str()] = kv.second; + } + } + } + + /** + * Build complete response with system info and labels + * @param labels Optional labels to include + */ + void buildCompleteResponse(const std::map& labels = {}) { + setSystemInfo(); + addLabels(labels); + } +}; + +/** + * Response class for node endpoints endpoint + * Handles complete JSON document creation for endpoint data + */ +class NodeEndpointsResponse : public CollectionResponse { +public: + NodeEndpointsResponse() : CollectionResponse("endpoints") {} + + /** + * Add a single endpoint to the response + * @param endpoint The EndpointInfo to add + */ + void addEndpoint(const EndpointInfo& endpoint) { + EndpointInfoSerializable serializable(endpoint); + addItem(serializable); + } + + /** + * Add multiple endpoints from a container + * @param endpoints Container of EndpointInfo objects + */ + void addEndpoints(const std::vector& endpoints) { + for (const auto& endpoint : endpoints) { + addEndpoint(endpoint); + } + } +}; + +/** + * Response class for simple status operations (update, restart) + * Handles simple JSON responses for node operations + */ +class NodeOperationResponse : public ApiResponse { +public: + /** + * Set success response + * @param status Status message + */ + void setSuccess(const String& status) { + doc["status"] = status; + } + + /** + * Set error response + * @param status Error status message + */ + void setError(const String& status) { + doc["status"] = status; + } +}; + +} // namespace types +} // namespace spore diff --git a/include/spore/types/TaskInfoSerializable.h b/include/spore/types/TaskInfoSerializable.h new file mode 100644 index 0000000..a2de430 --- /dev/null +++ b/include/spore/types/TaskInfoSerializable.h @@ -0,0 +1,96 @@ +#pragma once +#include "spore/util/JsonSerializable.h" +#include +#include + +namespace spore { +namespace types { + +/** + * Serializable wrapper for task information that implements JsonSerializable interface + * Handles conversion between task data and JSON representation + */ +class TaskInfoSerializable : public util::JsonSerializable { +private: + const String& taskName; + const JsonObject& taskData; + +public: + TaskInfoSerializable(const String& name, const JsonObject& data) + : taskName(name), taskData(data) {} + + /** + * Serialize task info to JsonObject + */ + void toJson(JsonObject& obj) const override { + obj["name"] = taskName; + obj["interval"] = taskData["interval"]; + obj["enabled"] = taskData["enabled"]; + obj["running"] = taskData["running"]; + obj["autoStart"] = taskData["autoStart"]; + } + + /** + * Deserialize task info from JsonObject + * Note: This is read-only for task status, so fromJson is not implemented + */ + void fromJson(const JsonObject& obj) override { + // Task info is typically read-only in this context + // Implementation would go here if needed + } +}; + +/** + * Serializable wrapper for system information + */ +class SystemInfoSerializable : public util::JsonSerializable { +public: + /** + * Serialize system info to JsonObject + */ + void toJson(JsonObject& obj) const override { + obj["freeHeap"] = ESP.getFreeHeap(); + obj["uptime"] = millis(); + } + + /** + * Deserialize system info from JsonObject + * Note: System info is typically read-only, so fromJson is not implemented + */ + void fromJson(const JsonObject& obj) override { + // System info is typically read-only + // Implementation would go here if needed + } +}; + +/** + * Serializable wrapper for task summary information + */ +class TaskSummarySerializable : public util::JsonSerializable { +private: + size_t totalTasks; + size_t activeTasks; + +public: + TaskSummarySerializable(size_t total, size_t active) + : totalTasks(total), activeTasks(active) {} + + /** + * Serialize task summary to JsonObject + */ + void toJson(JsonObject& obj) const override { + obj["totalTasks"] = totalTasks; + obj["activeTasks"] = activeTasks; + } + + /** + * Deserialize task summary from JsonObject + */ + void fromJson(const JsonObject& obj) override { + totalTasks = obj["totalTasks"].as(); + activeTasks = obj["activeTasks"].as(); + } +}; + +} // namespace types +} // namespace spore diff --git a/include/spore/types/TaskResponse.h b/include/spore/types/TaskResponse.h new file mode 100644 index 0000000..9e904b2 --- /dev/null +++ b/include/spore/types/TaskResponse.h @@ -0,0 +1,171 @@ +#pragma once +#include "ApiResponse.h" +#include "TaskInfoSerializable.h" +#include + +namespace spore { +namespace types { + +/** + * Response class for task status endpoint + * Handles complete JSON document creation for task status data + */ +class TaskStatusResponse : public ApiResponse { +public: + /** + * Set the task summary information + * @param totalTasks Total number of tasks + * @param activeTasks Number of active tasks + */ + void setSummary(size_t totalTasks, size_t activeTasks) { + TaskSummarySerializable summary(totalTasks, activeTasks); + JsonObject summaryObj = doc["summary"].to(); + summary.toJson(summaryObj); + } + + /** + * Add a single task to the response + * @param taskName Name of the task + * @param taskData Task data as JsonObject + */ + void addTask(const String& taskName, const JsonObject& taskData) { + TaskInfoSerializable serializable(taskName, taskData); + JsonArray tasksArr = doc["tasks"].to(); + JsonObject taskObj = tasksArr.add(); + serializable.toJson(taskObj); + } + + /** + * Add a single task to the response (std::string version) + * @param taskName Name of the task + * @param taskData Task data as JsonObject + */ + void addTask(const std::string& taskName, const JsonObject& taskData) { + TaskInfoSerializable serializable(String(taskName.c_str()), taskData); + JsonArray tasksArr = doc["tasks"].to(); + JsonObject taskObj = tasksArr.add(); + serializable.toJson(taskObj); + } + + /** + * Add multiple tasks from a task statuses map + * @param taskStatuses Map of task name to task data + */ + void addTasks(const std::map& taskStatuses) { + for (const auto& pair : taskStatuses) { + addTask(pair.first, pair.second); + } + } + + /** + * Set the system information + */ + void setSystemInfo() { + SystemInfoSerializable systemInfo; + JsonObject systemObj = doc["system"].to(); + systemInfo.toJson(systemObj); + } + + /** + * Build complete response with all components + * @param taskStatuses Map of task name to task data + */ + void buildCompleteResponse(const std::map& taskStatuses) { + // Set summary + size_t totalTasks = taskStatuses.size(); + size_t activeTasks = 0; + for (const auto& pair : taskStatuses) { + if (pair.second["enabled"]) { + activeTasks++; + } + } + setSummary(totalTasks, activeTasks); + + // Add all tasks + addTasks(taskStatuses); + + // Set system info + setSystemInfo(); + } + + /** + * Build complete response with all components from vector + * @param taskStatuses Vector of pairs of task name to task data + */ + void buildCompleteResponse(const std::vector>& taskStatuses) { + // Set summary + size_t totalTasks = taskStatuses.size(); + size_t activeTasks = 0; + for (const auto& pair : taskStatuses) { + if (pair.second["enabled"]) { + activeTasks++; + } + } + setSummary(totalTasks, activeTasks); + + // Add all tasks + for (const auto& pair : taskStatuses) { + addTask(pair.first, pair.second); + } + + // Set system info + setSystemInfo(); + } +}; + +/** + * Response class for task control operations + * Handles JSON responses for task enable/disable/start/stop operations + */ +class TaskControlResponse : public ApiResponse { +public: + /** + * Set the response data for a task control operation + * @param success Whether the operation was successful + * @param message Response message + * @param taskName Name of the task + * @param action Action performed + */ + void setResponse(bool success, const String& message, const String& taskName, const String& action) { + doc["success"] = success; + doc["message"] = message; + doc["task"] = taskName; + doc["action"] = action; + } + + /** + * Add detailed task information to the response + * @param taskName Name of the task + * @param enabled Whether task is enabled + * @param running Whether task is running + * @param interval Task interval + */ + void addTaskDetails(const String& taskName, bool enabled, bool running, unsigned long interval) { + JsonObject taskDetails = doc["taskDetails"].to(); + taskDetails["name"] = taskName; + taskDetails["enabled"] = enabled; + taskDetails["running"] = running; + taskDetails["interval"] = interval; + + // Add system info + SystemInfoSerializable systemInfo; + JsonObject systemObj = taskDetails["system"].to(); + systemInfo.toJson(systemObj); + } + + /** + * Set error response + * @param message Error message + * @param example Optional example for correct usage + */ + void setError(const String& message, const String& example = "") { + doc["success"] = false; + doc["message"] = message; + if (example.length() > 0) { + doc["example"] = example; + } + } +}; + +} // namespace types +} // namespace spore diff --git a/include/spore/util/JsonSerializable.h b/include/spore/util/JsonSerializable.h new file mode 100644 index 0000000..f584bc7 --- /dev/null +++ b/include/spore/util/JsonSerializable.h @@ -0,0 +1,56 @@ +#pragma once +#include + +namespace spore { +namespace util { + +/** + * Abstract base class for objects that can be serialized to/from JSON + * Provides a clean interface for converting objects to JsonObject and back + */ +class JsonSerializable { +public: + virtual ~JsonSerializable() = default; + + /** + * Serialize this object to a JsonObject + * @param obj The JsonObject to populate with this object's data + */ + virtual void toJson(JsonObject& obj) const = 0; + + /** + * Deserialize this object from a JsonObject + * @param obj The JsonObject containing the data to populate this object + */ + virtual void fromJson(const JsonObject& obj) = 0; + + /** + * Convenience method to create a JsonObject from this object + * @param doc The JsonDocument to create the object in + * @return A JsonObject containing this object's serialized data + */ + JsonObject toJsonObject(JsonDocument& doc) const { + JsonObject obj = doc.to(); + toJson(obj); + return obj; + } + + /** + * Convenience method to create a JsonArray from a collection of serializable objects + * @param doc The JsonDocument to create the array in + * @param objects Collection of objects implementing JsonSerializable + * @return A JsonArray containing all serialized objects + */ + template + static JsonArray toJsonArray(JsonDocument& doc, const Container& objects) { + JsonArray arr = doc.to(); + for (const auto& obj : objects) { + JsonObject item = arr.add(); + obj.toJson(item); + } + return arr; + } +}; + +} // namespace util +} // namespace spore diff --git a/src/spore/services/ClusterService.cpp b/src/spore/services/ClusterService.cpp index 12c7f18..d9a1d78 100644 --- a/src/spore/services/ClusterService.cpp +++ b/src/spore/services/ClusterService.cpp @@ -1,5 +1,8 @@ #include "spore/services/ClusterService.h" #include "spore/core/ApiServer.h" +#include "spore/types/ClusterResponse.h" + +using spore::types::ClusterMembersResponse; ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {} @@ -10,33 +13,7 @@ void ClusterService::registerEndpoints(ApiServer& api) { } void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) { - JsonDocument doc; - JsonArray arr = doc["members"].to(); - - for (const auto& pair : *ctx.memberList) { - const NodeInfo& node = pair.second; - JsonObject obj = arr.add(); - obj["hostname"] = node.hostname; - obj["ip"] = node.ip.toString(); - obj["lastSeen"] = node.lastSeen; - obj["latency"] = node.latency; - obj["status"] = statusToStr(node.status); - obj["resources"]["freeHeap"] = node.resources.freeHeap; - obj["resources"]["chipId"] = node.resources.chipId; - obj["resources"]["sdkVersion"] = node.resources.sdkVersion; - obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz; - obj["resources"]["flashChipSize"] = node.resources.flashChipSize; - - // Add labels if present - if (!node.labels.empty()) { - JsonObject labelsObj = obj["labels"].to(); - for (const auto& kv : node.labels) { - labelsObj[kv.first.c_str()] = kv.second; - } - } - } - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); + ClusterMembersResponse response; + response.addNodes(ctx.memberList); + request->send(200, "application/json", response.toJsonString()); } diff --git a/src/spore/services/NodeService.cpp b/src/spore/services/NodeService.cpp index 9da9015..50bb8d1 100644 --- a/src/spore/services/NodeService.cpp +++ b/src/spore/services/NodeService.cpp @@ -1,6 +1,11 @@ #include "spore/services/NodeService.h" #include "spore/core/ApiServer.h" #include "spore/util/Logging.h" +#include "spore/types/NodeResponse.h" + +using spore::types::NodeStatusResponse; +using spore::types::NodeOperationResponse; +using spore::types::NodeEndpointsResponse; NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {} @@ -32,40 +37,31 @@ void NodeService::registerEndpoints(ApiServer& api) { } void NodeService::handleStatusRequest(AsyncWebServerRequest* request) { - JsonDocument doc; - doc["freeHeap"] = ESP.getFreeHeap(); - doc["chipId"] = ESP.getChipId(); - doc["sdkVersion"] = ESP.getSdkVersion(); - doc["cpuFreqMHz"] = ESP.getCpuFreqMHz(); - doc["flashChipSize"] = ESP.getFlashChipSize(); - - // Include local node labels if present + NodeStatusResponse response; + + // Get labels from member list or self + std::map labels; if (ctx.memberList) { auto it = ctx.memberList->find(ctx.hostname); if (it != ctx.memberList->end()) { - JsonObject labelsObj = doc["labels"].to(); - for (const auto& kv : it->second.labels) { - labelsObj[kv.first.c_str()] = kv.second; - } + labels = it->second.labels; } else if (!ctx.self.labels.empty()) { - JsonObject labelsObj = doc["labels"].to(); - for (const auto& kv : ctx.self.labels) { - labelsObj[kv.first.c_str()] = kv.second; - } + labels = ctx.self.labels; } } - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); + + response.buildCompleteResponse(labels); + request->send(200, "application/json", response.toJsonString()); } void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) { bool success = !Update.hasError(); - AsyncWebServerResponse* response = request->beginResponse(200, "application/json", - success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}"); - response->addHeader("Connection", "close"); - request->send(response); + NodeOperationResponse response; + response.setSuccess(success ? "OK" : "FAIL"); + + AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString()); + httpResponse->addHeader("Connection", "close"); + request->send(httpResponse); request->onDisconnect([this]() { LOG_INFO("API", "Restart device"); delay(10); @@ -108,10 +104,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin } void NodeService::handleRestartRequest(AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse(200, "application/json", - "{\"status\": \"restarting\"}"); - response->addHeader("Connection", "close"); - request->send(response); + NodeOperationResponse response; + response.setSuccess("restarting"); + + AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString()); + httpResponse->addHeader("Connection", "close"); + request->send(httpResponse); request->onDisconnect([this]() { LOG_INFO("API", "Restart device"); delay(10); @@ -120,36 +118,7 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) { } void NodeService::handleEndpointsRequest(AsyncWebServerRequest* request) { - JsonDocument doc; - JsonArray endpointsArr = doc["endpoints"].to(); - - // Add all registered endpoints from ApiServer - for (const auto& endpoint : apiServer.getEndpoints()) { - JsonObject obj = endpointsArr.add(); - obj["uri"] = endpoint.uri; - obj["method"] = ApiServer::methodToStr(endpoint.method); - if (!endpoint.params.empty()) { - JsonArray paramsArr = obj["params"].to(); - for (const auto& ps : endpoint.params) { - JsonObject p = paramsArr.add(); - p["name"] = ps.name; - p["location"] = ps.location; - p["required"] = ps.required; - p["type"] = ps.type; - if (!ps.values.empty()) { - JsonArray allowed = p["values"].to(); - for (const auto& v : ps.values) { - allowed.add(v); - } - } - if (ps.defaultValue.length() > 0) { - p["default"] = ps.defaultValue; - } - } - } - } - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); + NodeEndpointsResponse response; + response.addEndpoints(apiServer.getEndpoints()); + request->send(200, "application/json", response.toJsonString()); } diff --git a/src/spore/services/TaskService.cpp b/src/spore/services/TaskService.cpp index c6db68e..b67d01d 100644 --- a/src/spore/services/TaskService.cpp +++ b/src/spore/services/TaskService.cpp @@ -1,7 +1,11 @@ #include "spore/services/TaskService.h" #include "spore/core/ApiServer.h" +#include "spore/types/TaskResponse.h" #include +using spore::types::TaskStatusResponse; +using spore::types::TaskControlResponse; + TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {} void TaskService::registerEndpoints(ApiServer& api) { @@ -35,29 +39,9 @@ void TaskService::handleStatusRequest(AsyncWebServerRequest* request) { JsonDocument scratch; auto taskStatuses = taskManager.getAllTaskStatuses(scratch); - JsonDocument doc; - JsonObject summaryObj = doc["summary"].to(); - summaryObj["totalTasks"] = taskStatuses.size(); - summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(), - [](const auto& pair) { return pair.second["enabled"]; }); - - JsonArray tasksArr = doc["tasks"].to(); - for (const auto& taskPair : taskStatuses) { - JsonObject taskObj = tasksArr.add(); - taskObj["name"] = taskPair.first; - taskObj["interval"] = taskPair.second["interval"]; - taskObj["enabled"] = taskPair.second["enabled"]; - taskObj["running"] = taskPair.second["running"]; - taskObj["autoStart"] = taskPair.second["autoStart"]; - } - - JsonObject systemObj = doc["system"].to(); - systemObj["freeHeap"] = ESP.getFreeHeap(); - systemObj["uptime"] = millis(); - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); + TaskStatusResponse response; + response.buildCompleteResponse(taskStatuses); + request->send(200, "application/json", response.toJsonString()); } void TaskService::handleControlRequest(AsyncWebServerRequest* request) { @@ -88,50 +72,27 @@ void TaskService::handleControlRequest(AsyncWebServerRequest* request) { success = true; message = "Task status retrieved"; - JsonDocument statusDoc; - statusDoc["success"] = success; - statusDoc["message"] = message; - statusDoc["task"] = taskName; - statusDoc["action"] = action; + TaskControlResponse response; + response.setResponse(success, message, taskName, action); + response.addTaskDetails(taskName, + taskManager.isTaskEnabled(taskName.c_str()), + taskManager.isTaskRunning(taskName.c_str()), + taskManager.getTaskInterval(taskName.c_str())); - statusDoc["taskDetails"] = JsonObject(); - JsonObject taskDetails = statusDoc["taskDetails"]; - taskDetails["name"] = taskName; - taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str()); - taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str()); - taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str()); - - taskDetails["system"] = JsonObject(); - JsonObject systemInfo = taskDetails["system"]; - systemInfo["freeHeap"] = ESP.getFreeHeap(); - systemInfo["uptime"] = millis(); - - String statusJson; - serializeJson(statusDoc, statusJson); - request->send(200, "application/json", statusJson); + request->send(200, "application/json", response.toJsonString()); return; } else { success = false; message = "Invalid action. Use: enable, disable, start, stop, or status"; } - JsonDocument doc; - doc["success"] = success; - doc["message"] = message; - doc["task"] = taskName; - doc["action"] = action; - - String json; - serializeJson(doc, json); - request->send(success ? 200 : 400, "application/json", json); + TaskControlResponse response; + response.setResponse(success, message, taskName, action); + request->send(success ? 200 : 400, "application/json", response.toJsonString()); } else { - JsonDocument doc; - doc["success"] = false; - doc["message"] = "Missing parameters. Required: task, action"; - doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}"; - - String json; - serializeJson(doc, json); - request->send(400, "application/json", json); + TaskControlResponse response; + response.setError("Missing parameters. Required: task, action", + "{\"task\": \"discovery_send\", \"action\": \"status\"}"); + request->send(400, "application/json", response.toJsonString()); } }