From 862b2caf5beb72dd063032a0220b447951374ad6 Mon Sep 17 00:00:00 2001 From: Hans-Joachim Krauch Date: Wed, 20 Mar 2024 22:15:34 -0300 Subject: [PATCH] apply spec updates to c++ server implementation (#709) ### Public-Facing Changes Apply changes wrt. service definition to example server implementations ### Description - [X] C++ - [X] Python - [X] ~Typescript~ (done in #579) --- .../include/foxglove/websocket/common.hpp | 16 +++++- .../foxglove/websocket/serialization.hpp | 2 + .../foxglove/websocket/websocket_server.hpp | 9 ++++ cpp/foxglove-websocket/src/serialization.cpp | 49 +++++++++++++++++-- .../examples/json_server.py | 46 ++++++++++------- .../src/foxglove_websocket/server/__init__.py | 9 ++++ python/src/foxglove_websocket/types.py | 17 ++++++- python/tests/test_server.py | 44 +++++++++++------ 8 files changed, 151 insertions(+), 41 deletions(-) diff --git a/cpp/foxglove-websocket/include/foxglove/websocket/common.hpp b/cpp/foxglove-websocket/include/foxglove/websocket/common.hpp index 29ae852d..3af5801e 100644 --- a/cpp/foxglove-websocket/include/foxglove/websocket/common.hpp +++ b/cpp/foxglove-websocket/include/foxglove/websocket/common.hpp @@ -111,11 +111,23 @@ struct ClientMessage { } }; +struct ServiceRequestDefinition { + std::string encoding; + std::string schemaName; + std::string schemaEncoding; + std::string schema; +}; + +using ServiceResponseDefinition = ServiceRequestDefinition; + struct ServiceWithoutId { std::string name; std::string type; - std::string requestSchema; - std::string responseSchema; + std::optional request; + std::optional response; + + std::optional requestSchema; // Prefer request instead + std::optional responseSchema; // Prefer response instead }; struct Service : ServiceWithoutId { diff --git a/cpp/foxglove-websocket/include/foxglove/websocket/serialization.hpp b/cpp/foxglove-websocket/include/foxglove/websocket/serialization.hpp index 87893239..0712c7ac 100644 --- a/cpp/foxglove-websocket/include/foxglove/websocket/serialization.hpp +++ b/cpp/foxglove-websocket/include/foxglove/websocket/serialization.hpp @@ -52,5 +52,7 @@ void to_json(nlohmann::json& j, const Parameter& p); void from_json(const nlohmann::json& j, Parameter& p); void to_json(nlohmann::json& j, const Service& p); void from_json(const nlohmann::json& j, Service& p); +void to_json(nlohmann::json& j, const ServiceRequestDefinition& p); +void from_json(const nlohmann::json& j, ServiceRequestDefinition& p); } // namespace foxglove diff --git a/cpp/foxglove-websocket/include/foxglove/websocket/websocket_server.hpp b/cpp/foxglove-websocket/include/foxglove/websocket/websocket_server.hpp index 959656ce..b1eb8169 100644 --- a/cpp/foxglove-websocket/include/foxglove/websocket/websocket_server.hpp +++ b/cpp/foxglove-websocket/include/foxglove/websocket/websocket_server.hpp @@ -906,6 +906,15 @@ inline std::vector Server::addServices( std::vector serviceIds; json newServices; for (const auto& service : services) { + if (!service.request.has_value() && !service.requestSchema.has_value()) { + throw std::runtime_error( + "Invalid service definition: Either `request` or `requestSchema` must be defined"); + } + if (!service.response.has_value() && !service.responseSchema.has_value()) { + throw std::runtime_error( + "Invalid service definition: Either `response` or `responseSchema` must be defined"); + } + const ServiceId serviceId = ++_nextServiceId; _services.emplace(serviceId, service); serviceIds.push_back(serviceId); diff --git a/cpp/foxglove-websocket/src/serialization.cpp b/cpp/foxglove-websocket/src/serialization.cpp index 1058b278..29d21b52 100644 --- a/cpp/foxglove-websocket/src/serialization.cpp +++ b/cpp/foxglove-websocket/src/serialization.cpp @@ -103,17 +103,58 @@ void to_json(nlohmann::json& j, const Service& service) { {"id", service.id}, {"name", service.name}, {"type", service.type}, - {"requestSchema", service.requestSchema}, - {"responseSchema", service.responseSchema}, }; + + if (service.request) { + j["request"] = *service.request; + } + if (service.response) { + j["response"] = *service.response; + } + if (service.requestSchema) { + j["requestSchema"] = *service.requestSchema; + } + if (service.responseSchema) { + j["responseSchema"] = *service.responseSchema; + } } void from_json(const nlohmann::json& j, Service& p) { p.id = j["id"].get(); p.name = j["name"].get(); p.type = j["type"].get(); - p.requestSchema = j["requestSchema"].get(); - p.responseSchema = j["responseSchema"].get(); + + if (const auto it = j.find("request"); it != j.end()) { + p.request = it->get(); + } + + if (const auto it = j.find("response"); it != j.end()) { + p.response = it->get(); + } + + if (const auto it = j.find("requestSchema"); it != j.end()) { + p.requestSchema = it->get(); + } + + if (const auto it = j.find("responseSchema"); it != j.end()) { + p.responseSchema = it->get(); + } +} + +void to_json(nlohmann::json& j, const ServiceRequestDefinition& r) { + j = { + {"encoding", r.encoding}, + {"schemaName", r.schemaName}, + {"schemaEncoding", r.schemaEncoding}, + {"schema", r.schema}, + }; +} + +void from_json(const nlohmann::json& j, ServiceRequestDefinition& r) { + r.encoding = j["encoding"].get(); + r.schemaName = j["schemaName"].get(); + r.schemaEncoding = j["schemaEncoding"].get(); + r.schema = j["schema"].get(); } void ServiceResponse::read(const uint8_t* data, size_t dataLength) { diff --git a/python/src/foxglove_websocket/examples/json_server.py b/python/src/foxglove_websocket/examples/json_server.py index 42f0244b..98baf026 100644 --- a/python/src/foxglove_websocket/examples/json_server.py +++ b/python/src/foxglove_websocket/examples/json_server.py @@ -87,23 +87,35 @@ async def on_service_request( await server.add_service( { "name": "example_set_bool", - "requestSchema": json.dumps( - { - "type": "object", - "properties": { - "data": {"type": "boolean"}, - }, - } - ), - "responseSchema": json.dumps( - { - "type": "object", - "properties": { - "success": {"type": "boolean"}, - "message": {"type": "string"}, - }, - } - ), + "request": { + "encoding": "json", + "schemaName": "requestSchema", + "schemaEncoding": "jsonschema", + "schema": json.dumps( + { + "type": "object", + "properties": { + "data": {"type": "boolean"}, + }, + } + ), + }, + "response": { + "encoding": "json", + "schemaName": "responseSchema", + "schemaEncoding": "jsonschema", + "schema": json.dumps( + { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + "message": {"type": "string"}, + }, + } + ), + }, + "requestSchema": None, + "responseSchema": None, "type": "example_set_bool", } ) diff --git a/python/src/foxglove_websocket/server/__init__.py b/python/src/foxglove_websocket/server/__init__.py index f0c3219c..696359d2 100644 --- a/python/src/foxglove_websocket/server/__init__.py +++ b/python/src/foxglove_websocket/server/__init__.py @@ -271,6 +271,15 @@ async def remove_channel(self, chan_id: ChannelId): ) async def add_service(self, service: ServiceWithoutId) -> ServiceId: + if "request" not in service.keys() and "requestSchema" not in service.keys(): + raise ValueError( + f"Invalid service definition: Either 'request' or 'requestSchema' must be defined" + ) + if "response" not in service.keys() and "responseSchema" not in service.keys(): + raise ValueError( + f"Invalid service definition: Either 'response' or 'responseSchema' must be defined" + ) + new_id = self._next_service_id self._next_service_id = ServiceId(new_id + 1) new_service = Service(id=new_id, **service) diff --git a/python/src/foxglove_websocket/types.py b/python/src/foxglove_websocket/types.py index 4d01e901..0f5cf683 100644 --- a/python/src/foxglove_websocket/types.py +++ b/python/src/foxglove_websocket/types.py @@ -142,11 +142,24 @@ class Channel(ChannelWithoutId): id: ChannelId +class ServiceRequestDefinition(TypedDict): + encoding: str + schemaName: str + schemaEncoding: str + schema: str + + +class ServiceResponseDefinition(ServiceRequestDefinition): + pass + + class ServiceWithoutId(TypedDict): name: str type: str - requestSchema: str - responseSchema: str + request: Optional[ServiceRequestDefinition] + response: Optional[ServiceResponseDefinition] + requestSchema: Optional[str] # Prefer request instead + responseSchema: Optional[str] # Prefer response instead class Service(ServiceWithoutId): diff --git a/python/tests/test_server.py b/python/tests/test_server.py index 296bae02..1dcb46d6 100644 --- a/python/tests/test_server.py +++ b/python/tests/test_server.py @@ -311,22 +311,34 @@ async def on_service_request( service: ServiceWithoutId = { "name": "set_bool", "type": "set_bool", - "requestSchema": json.dumps( - { - "type": "object", - "properties": { - "data": {"type": "boolean"}, - }, - } - ), - "responseSchema": json.dumps( - { - "type": "object", - "properties": { - "success": {"type": "boolean"}, - }, - } - ), + "request": { + "encoding": "json", + "schemaName": "requestSchema", + "schemaEncoding": "jsonschema", + "schema": json.dumps( + { + "type": "object", + "properties": { + "data": {"type": "boolean"}, + }, + } + ), + }, + "response": { + "encoding": "json", + "schemaName": "responseSchema", + "schemaEncoding": "jsonschema", + "schema": json.dumps( + { + "type": "object", + "properties": { + "success": {"type": "boolean"}, + }, + } + ), + }, + "requestSchema": None, + "responseSchema": None, } service_id = await server.add_service(service)