Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions docs/api/rest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,13 @@ Manage ROS 2 node parameters.
{
"items": [
{
"id": "publish_rate",
"name": "publish_rate",
"value": 10.0,
"type": "double",
"description": "Publishing rate in Hz"
"type": "double"
},
{
"id": "sensor_id",
"name": "sensor_id",
"value": "sensor_001",
"type": "string"
}
],
Expand All @@ -486,7 +485,7 @@ Manage ROS 2 node parameters.

curl -X PUT http://localhost:8080/api/v1/components/temp_sensor/configurations/publish_rate \
-H "Content-Type: application/json" \
-d '{"value": 20.0}'
-d '{"data": 20.0}'

``DELETE /api/v1/components/{id}/configurations/{param_name}``
Reset parameter to default value.
Expand Down
8 changes: 4 additions & 4 deletions docs/tutorials/demos/demo-sensor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ View and modify sensor parameters:
# Change scan rate
curl -X PUT http://localhost:8080/api/v1/apps/lidar-sim/configurations/scan_rate \
-H "Content-Type: application/json" \
-d '{"value": 20.0}'
-d '{"data": 20.0}'

**Key Parameters:**

Expand Down Expand Up @@ -189,15 +189,15 @@ You can also inject faults by setting parameters directly:

# Increase noise level
curl -X PUT http://localhost:8080/api/v1/apps/lidar-sim/configurations/noise_stddev \
-H "Content-Type: application/json" -d '{"value": 0.5}'
-H "Content-Type: application/json" -d '{"data": 0.5}'

# Enable NaN injection
curl -X PUT http://localhost:8080/api/v1/apps/imu-sim/configurations/inject_nan \
-H "Content-Type: application/json" -d '{"value": true}'
-H "Content-Type: application/json" -d '{"data": true}'

# Increase failure probability
curl -X PUT http://localhost:8080/api/v1/apps/gps-sim/configurations/failure_probability \
-H "Content-Type: application/json" -d '{"value": 0.3}'
-H "Content-Type: application/json" -d '{"data": 0.3}'

Stopping the Demo
-----------------
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/locking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ response until the lock is released or expires.
curl -X PUT http://localhost:8080/api/v1/components/motor_controller/configurations/max_speed \
-H "Content-Type: application/json" \
-H "X-Client-Id: $CLIENT_ID" \
-d '{"value": 1500}'
-d '{"data": 1500}'

**3. Extend the lock**

Expand Down
25 changes: 13 additions & 12 deletions src/ros2_medkit_gateway/src/http/rest_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ void RESTServer::setup_routes() {
.summary(std::string("Get operation details for ") + et.singular)
.description(std::string("Returns operation details including request/response schema for this ") +
et.singular + ".")
.response(200, "Operation details", SB::ref("OperationItem"))
.response(200, "Operation details", SB::ref("OperationDetail"))
.operation_id(std::string("get") + capitalize(et.singular) + "Operation");

// Execution endpoints
Expand Down Expand Up @@ -546,7 +546,7 @@ void RESTServer::setup_routes() {
.tag("Configuration")
.summary(std::string("List configurations for ") + et.singular)
.description(std::string("Lists all ROS 2 node parameters for this ") + et.singular + ".")
.response(200, "Configuration list", SB::ref("ConfigurationParamList"))
.response(200, "Configuration list", SB::ref("ConfigurationMetaDataList"))
.operation_id(std::string("list") + capitalize(et.singular) + "Configurations");

reg.get(entity_path + "/configurations/{config_id}",
Expand All @@ -556,7 +556,7 @@ void RESTServer::setup_routes() {
.tag("Configuration")
.summary(std::string("Get specific configuration for ") + et.singular)
.description(std::string("Returns a specific ROS 2 node parameter for this ") + et.singular + ".")
.response(200, "Configuration parameter", SB::ref("ConfigurationParam"))
.response(200, "Configuration parameter", SB::ref("ConfigurationReadValue"))
.operation_id(std::string("get") + capitalize(et.singular) + "Configuration");

reg.put(entity_path + "/configurations/{config_id}",
Expand All @@ -566,8 +566,8 @@ void RESTServer::setup_routes() {
.tag("Configuration")
.summary(std::string("Set configuration for ") + et.singular)
.description(std::string("Sets a ROS 2 node parameter value for this ") + et.singular + ".")
.request_body("Configuration value", SB::ref("ConfigurationParam"))
.response(200, "Updated configuration", SB::ref("ConfigurationParam"))
.request_body("Configuration value", SB::ref("ConfigurationWriteValue"))
.response(200, "Updated configuration", SB::ref("ConfigurationReadValue"))
.operation_id(std::string("set") + capitalize(et.singular) + "Configuration");

reg.del(entity_path + "/configurations/{config_id}",
Expand All @@ -588,6 +588,7 @@ void RESTServer::setup_routes() {
.summary(std::string("Delete all configurations for ") + et.singular)
.description(std::string("Resets all configuration parameters for this ") + et.singular + ".")
.response(204, "All configurations deleted")
.response(207, "Partial success - some nodes failed", SB::ref("ConfigurationDeleteMultiStatus"))
.operation_id(std::string("deleteAll") + capitalize(et.singular) + "Configurations");

// --- Faults ---
Expand Down Expand Up @@ -772,7 +773,7 @@ void RESTServer::setup_routes() {
.tag("Triggers")
.summary(std::string("Create trigger for ") + et.singular)
.description(std::string("Creates a new event trigger for this ") + et.singular + ".")
.request_body("Trigger configuration", SB::ref("Trigger"))
.request_body("Trigger configuration", SB::ref("TriggerCreateRequest"))
.response(201, "Trigger created", SB::ref("Trigger"))
.operation_id(std::string("create") + capitalize(et.singular) + "Trigger");

Expand Down Expand Up @@ -815,7 +816,7 @@ void RESTServer::setup_routes() {
.tag("Triggers")
.summary(std::string("Update trigger for ") + et.singular)
.description(std::string("Updates a trigger configuration on this ") + et.singular + ".")
.request_body("Trigger update", SB::ref("Trigger"))
.request_body("Trigger update", SB::ref("TriggerUpdateRequest"))
.response(200, "Updated trigger", SB::ref("Trigger"))
.operation_id(std::string("update") + capitalize(et.singular) + "Trigger");

Expand Down Expand Up @@ -853,7 +854,7 @@ void RESTServer::setup_routes() {
.tag("Subscriptions")
.summary(std::string("Create cyclic subscription for ") + et.singular)
.description(std::string("Creates a new cyclic data subscription for this ") + et.singular + ".")
.request_body("Subscription configuration", SB::ref("CyclicSubscription"))
.request_body("Subscription configuration", SB::ref("CyclicSubscriptionCreateRequest"))
.response(201, "Subscription created", SB::ref("CyclicSubscription"))
.operation_id(std::string("create") + capitalize(et.singular) + "Subscription");

Expand Down Expand Up @@ -964,7 +965,7 @@ void RESTServer::setup_routes() {
.summary(std::string("Upload diagnostic script for ") + et.singular)
.description(std::string("Uploads a diagnostic script for this ") + et.singular + ".")
.request_body("Script file", SB::binary_schema(), "multipart/form-data")
.response(201, "Script uploaded", SB::ref("ScriptMetadata"))
.response(201, "Script uploaded", SB::ref("ScriptUploadResponse"))
.operation_id(std::string("upload") + capitalize(et.singular) + "Script");

reg.get(entity_path + "/scripts",
Expand Down Expand Up @@ -1286,7 +1287,7 @@ void RESTServer::setup_routes() {
.summary("Prepare update for execution")
.description("Prepares an update for execution (downloads, validates).")
.request_body("Prepare parameters", SB::generic_object_schema())
.response(200, "Update prepared", SB::ref("UpdateStatus"))
.response(202, "Update preparation started")
.operation_id("prepareUpdate");

reg.put("/updates/{update_id}/execute", update_handlers_ ? HandlerFn([this](auto & req, auto & res) {
Expand All @@ -1297,7 +1298,7 @@ void RESTServer::setup_routes() {
.summary("Execute update")
.description("Starts executing a prepared update.")
.request_body("Execute parameters", SB::generic_object_schema())
.response(200, "Update executing", SB::ref("UpdateStatus"))
.response(202, "Update execution started")
.operation_id("executeUpdate");

reg.put("/updates/{update_id}/automated", update_handlers_ ? HandlerFn([this](auto & req, auto & res) {
Expand All @@ -1308,7 +1309,7 @@ void RESTServer::setup_routes() {
.summary("Run automated update")
.description("Runs a fully automated update (prepare + execute).")
.request_body("Automated parameters", SB::generic_object_schema())
.response(200, "Automated update started", SB::ref("UpdateStatus"))
.response(202, "Automated update started")
.operation_id("automateUpdate");

reg.get("/updates/{update_id}", update_handlers_ ? HandlerFn([this](auto & req, auto & res) {
Expand Down
63 changes: 16 additions & 47 deletions src/ros2_medkit_gateway/src/openapi/path_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,9 @@ nlohmann::json PathBuilder::build_data_collection(const std::string & entity_pat
get_op["description"] = "Returns all available data items (topics) for this entity.";
get_op["parameters"] = build_query_params_for_collection();

// Use the standard items wrapper with a generic data item schema
nlohmann::json data_item_schema = {{"type", "object"},
{"properties",
{{"name", {{"type", "string"}}},
{"type", {{"type", "string"}}},
{"direction", {{"type", "string"}}},
{"uri", {{"type", "string"}}}}}};
get_op["responses"]["200"]["description"] = "Successful response";
get_op["responses"]["200"]["content"]["application/json"]["schema"] = SchemaBuilder::items_wrapper(data_item_schema);
get_op["responses"]["200"]["content"]["application/json"]["schema"] =
SchemaBuilder::items_wrapper(SchemaBuilder::data_item_schema());

auto errors = error_responses();
for (auto & [code, val] : errors.items()) {
Expand Down Expand Up @@ -147,8 +141,7 @@ nlohmann::json PathBuilder::build_data_item(const std::string & /*entity_path*/,
put_op["requestBody"]["required"] = true;
put_op["requestBody"]["content"]["application/json"]["schema"] = schema_builder_.from_ros_msg(topic.type);
put_op["responses"]["200"]["description"] = "Value written successfully";
put_op["responses"]["200"]["content"]["application/json"]["schema"] = {
{"type", "object"}, {"properties", {{"status", {{"type", "string"}}}}}};
put_op["responses"]["200"]["content"]["application/json"]["schema"] = SchemaBuilder::generic_object_schema();

Comment on lines 141 to 145
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For PUT .../data/{item}, the OpenAPI schemas here still don't match the real handler contract: the handler expects a wrapper body containing {type: string, data: <msg-json>} (not the raw ROS message schema), and it responds with an object containing at least id and data (plus x-medkit). Documenting both request and 200-response as from_ros_msg(...) / generic_object_schema() will cause generated clients to send/expect the wrong shapes.

Copilot uses AI. Check for mistakes.
auto put_errors = error_responses();
for (auto & [code, val] : put_errors.items()) {
Expand Down Expand Up @@ -181,15 +174,9 @@ nlohmann::json PathBuilder::build_operations_collection(const std::string & enti
get_op["description"] = "Returns all available operations (services and actions) for this entity.";
get_op["parameters"] = build_query_params_for_collection();

nlohmann::json op_item_schema = {{"type", "object"},
{"properties",
{{"name", {{"type", "string"}}},
{"type", {{"type", "string"}}},
{"kind", {{"type", "string"}, {"enum", {"service", "action"}}}},
{"path", {{"type", "string"}}}}}};

get_op["responses"]["200"]["description"] = "Successful response";
get_op["responses"]["200"]["content"]["application/json"]["schema"] = SchemaBuilder::items_wrapper(op_item_schema);
get_op["responses"]["200"]["content"]["application/json"]["schema"] =
SchemaBuilder::items_wrapper(SchemaBuilder::operation_item_schema());

auto errors = error_responses();
for (auto & [code, val] : errors.items()) {
Expand Down Expand Up @@ -280,8 +267,7 @@ nlohmann::json PathBuilder::build_operation_item(const std::string & /*entity_pa
post_op["requestBody"]["content"]["application/json"]["schema"] =
schema_builder_.from_ros_msg(action.type + "_SendGoal_Request");
post_op["responses"]["202"]["description"] = "Action accepted";
post_op["responses"]["202"]["content"]["application/json"]["schema"] = {
{"type", "object"}, {"properties", {{"id", {{"type", "string"}}}, {"status", {{"type", "string"}}}}}};
post_op["responses"]["202"]["content"]["application/json"]["schema"] = SchemaBuilder::operation_execution_schema();

auto post_errors = error_responses();
for (auto & [code, val] : post_errors.items()) {
Expand Down Expand Up @@ -309,7 +295,7 @@ nlohmann::json PathBuilder::build_configurations_collection(const std::string &
get_op["parameters"] = build_query_params_for_collection();
get_op["responses"]["200"]["description"] = "Successful response";
get_op["responses"]["200"]["content"]["application/json"]["schema"] =
SchemaBuilder::items_wrapper(SchemaBuilder::configuration_param_schema());
SchemaBuilder::items_wrapper(SchemaBuilder::configuration_metadata_schema());

auto errors = error_responses();
for (auto & [code, val] : errors.items()) {
Expand All @@ -323,8 +309,10 @@ nlohmann::json PathBuilder::build_configurations_collection(const std::string &
delete_op["tags"] = nlohmann::json::array({"Configuration"});
delete_op["summary"] = "Delete all configuration parameters";
delete_op["description"] = "Delete all configuration parameters for this entity, resetting them to defaults.";
delete_op["responses"]["200"]["description"] = "All parameters deleted";
delete_op["responses"]["200"]["content"]["application/json"]["schema"] = SchemaBuilder::configuration_param_schema();
delete_op["responses"]["204"]["description"] = "All parameters deleted";
delete_op["responses"]["207"]["description"] = "Partial success - some nodes failed";
delete_op["responses"]["207"]["content"]["application/json"]["schema"] =
SchemaBuilder::configuration_delete_multi_status_schema();

auto del_errors = error_responses();
for (auto & [code, val] : del_errors.items()) {
Expand Down Expand Up @@ -426,13 +414,7 @@ nlohmann::json PathBuilder::build_bulk_data_collection(const std::string & entit
get_op["parameters"] = build_query_params_for_collection();
get_op["responses"]["200"]["description"] = "Successful response";
get_op["responses"]["200"]["content"]["application/json"]["schema"] =
SchemaBuilder::items_wrapper({{"type", "object"},
{"properties",
{{"id", {{"type", "string"}}},
{"name", {{"type", "string"}}},
{"status", {{"type", "string"}}},
{"created_at", {{"type", "string"}}},
{"size_bytes", {{"type", "integer"}}}}}});
SchemaBuilder::items_wrapper(SchemaBuilder::bulk_data_descriptor_schema());

auto errors = error_responses();
for (auto & [code, val] : errors.items()) {
Expand All @@ -458,12 +440,7 @@ nlohmann::json PathBuilder::build_cyclic_subscriptions_collection(const std::str
get_op["parameters"] = build_query_params_for_collection();
get_op["responses"]["200"]["description"] = "Successful response";
get_op["responses"]["200"]["content"]["application/json"]["schema"] =
SchemaBuilder::items_wrapper({{"type", "object"},
{"properties",
{{"id", {{"type", "string"}}},
{"topic", {{"type", "string"}}},
{"interval_ms", {{"type", "integer"}}},
{"status", {{"type", "string"}}}}}});
SchemaBuilder::items_wrapper(SchemaBuilder::cyclic_subscription_schema());

auto errors = error_responses();
for (auto & [code, val] : errors.items()) {
Expand All @@ -478,18 +455,10 @@ nlohmann::json PathBuilder::build_cyclic_subscriptions_collection(const std::str
post_op["summary"] = "Create cyclic subscription";
post_op["description"] = "Create a new cyclic subscription to stream data changes via SSE.";
post_op["requestBody"]["required"] = true;
post_op["requestBody"]["content"]["application/json"]["schema"] = {
{"type", "object"},
{"properties", {{"topic", {{"type", "string"}}}, {"interval_ms", {{"type", "integer"}, {"minimum", 1}}}}},
{"required", {"topic"}}};
post_op["requestBody"]["content"]["application/json"]["schema"] =
SchemaBuilder::cyclic_subscription_create_request_schema();
post_op["responses"]["201"]["description"] = "Subscription created";
post_op["responses"]["201"]["content"]["application/json"]["schema"] = {{"type", "object"},
{"properties",
{{"id", {{"type", "string"}}},
{"topic", {{"type", "string"}}},
{"interval_ms", {{"type", "integer"}}},
{"status", {{"type", "string"}}},
{"stream_uri", {{"type", "string"}}}}}};
post_op["responses"]["201"]["content"]["application/json"]["schema"] = SchemaBuilder::cyclic_subscription_schema();

auto post_errors = error_responses();
for (auto & [code, val] : post_errors.items()) {
Expand Down
Loading
Loading