diff --git a/docs/README.md b/docs/README.md index 0abfe2b..09d5817 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,7 +20,7 @@ The `architecture.puml` file contains a PlantUML class diagram showing the relat - Extracts the entity hierarchy from the ROS 2 graph 3. **RESTServer** - Provides the HTTP/REST API - - Serves endpoints: `/health`, `/`, `/areas` + - Serves endpoints: `/health`, `/`, `/areas`, `/components`, `/areas/{area_id}/components` - Retrieves cached entities from the GatewayNode - Runs on configurable host and port diff --git a/postman/README.md b/postman/README.md index edf61a0..8711f44 100644 --- a/postman/README.md +++ b/postman/README.md @@ -10,6 +10,7 @@ Includes below endpoints: - ✅ GET `/` - Gateway info - ✅ GET `/areas` - List all areas - ✅ GET `/components` - List all components +- ✅ GET `/areas/{area_id}/components` - List components in specific area ## Quick Start @@ -54,6 +55,10 @@ ros2 launch ros2_medkit_gateway demo_nodes.launch.py 9. Click **Send** 10. You should see components: `[{"id": "temp_sensor", "namespace": "/powertrain/engine", ...}, ...]` +11. Click **"GET Area Components"** +12. Click **Send** +13. You should see only powertrain components: `[{"id": "temp_sensor", "area": "powertrain", ...}, ...]` + ## API Variables The environment includes: diff --git a/postman/collections/ros2-medkit-gateway.postman_collection.json b/postman/collections/ros2-medkit-gateway.postman_collection.json index 07372af..12a236c 100644 --- a/postman/collections/ros2-medkit-gateway.postman_collection.json +++ b/postman/collections/ros2-medkit-gateway.postman_collection.json @@ -61,6 +61,26 @@ "description": "List all discovered components across all areas. Returns component metadata including id, namespace, fqn, type, and parent area." }, "response": [] + }, + { + "name": "GET Area Components", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/areas/powertrain/components", + "host": [ + "{{base_url}}" + ], + "path": [ + "areas", + "powertrain", + "components" + ] + }, + "description": "List components within a specific area. Returns 404 if area doesn't exist. Change 'powertrain' to other areas like 'chassis', 'body', etc." + }, + "response": [] } ] } diff --git a/src/ros2_medkit_gateway/README.md b/src/ros2_medkit_gateway/README.md index 355cc13..7d472af 100644 --- a/src/ros2_medkit_gateway/README.md +++ b/src/ros2_medkit_gateway/README.md @@ -20,6 +20,7 @@ The ROS 2 Medkit Gateway exposes ROS 2 system information and data through a RES - `GET /` - Gateway status and version information - `GET /areas` - List all discovered areas (powertrain, chassis, body, root) - `GET /components` - List all discovered components across all areas +- `GET /areas/{area_id}/components` - List components within a specific area ### API Reference @@ -84,6 +85,56 @@ curl http://localhost:8080/components - `type` - Always "Component" - `area` - Parent area this component belongs to +#### GET /areas/{area_id}/components + +Lists all components within a specific area. + +**Example (Success):** +```bash +curl http://localhost:8080/areas/powertrain/components +``` + +**Response (200 OK):** +```json +[ + { + "id": "temp_sensor", + "namespace": "/powertrain/engine", + "fqn": "/powertrain/engine/temp_sensor", + "type": "Component", + "area": "powertrain" + }, + { + "id": "rpm_sensor", + "namespace": "/powertrain/engine", + "fqn": "/powertrain/engine/rpm_sensor", + "type": "Component", + "area": "powertrain" + } +] +``` + +**Example (Error - Area Not Found):** +```bash +curl http://localhost:8080/areas/nonexistent/components +``` + +**Response (404 Not Found):** +```json +{ + "error": "Area not found", + "area_id": "nonexistent" +} +``` + +**URL Parameters:** +- `area_id` - Area identifier (e.g., `powertrain`, `chassis`, `body`) + +**Use Cases:** +- Filter components by domain (only show powertrain components) +- Hierarchical navigation (select area → view its components) +- Area-specific health checks + ## Quick Start ### Build diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp index 885e228..cfa7872 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp @@ -41,6 +41,7 @@ class RESTServer { void handle_root(const httplib::Request& req, httplib::Response& res); void handle_list_areas(const httplib::Request& req, httplib::Response& res); void handle_list_components(const httplib::Request& req, httplib::Response& res); + void handle_area_components(const httplib::Request& req, httplib::Response& res); GatewayNode* node_; std::string host_; diff --git a/src/ros2_medkit_gateway/src/rest_server.cpp b/src/ros2_medkit_gateway/src/rest_server.cpp index 44eaeb5..075769e 100644 --- a/src/ros2_medkit_gateway/src/rest_server.cpp +++ b/src/ros2_medkit_gateway/src/rest_server.cpp @@ -51,6 +51,11 @@ void RESTServer::setup_routes() { server_->Get("/components", [this](const httplib::Request& req, httplib::Response& res) { handle_list_components(req, res); }); + + // Area components + server_->Get(R"(/areas/([^/]+)/components)", [this](const httplib::Request& req, httplib::Response& res) { + handle_area_components(req, res); + }); } void RESTServer::start() { @@ -160,4 +165,63 @@ void RESTServer::handle_list_components(const httplib::Request& req, httplib::Re } } +void RESTServer::handle_area_components(const httplib::Request& req, httplib::Response& res) { + try { + // Extract area_id from URL path + if (req.matches.size() < 2) { + res.status = 400; + res.set_content( + json{{"error", "Invalid request"}}.dump(2), + "application/json" + ); + return; + } + + std::string area_id = req.matches[1]; + const auto cache = node_->get_entity_cache(); + + // Check if area exists + bool area_exists = false; + for (const auto& area : cache.areas) { + if (area.id == area_id) { + area_exists = true; + break; + } + } + + if (!area_exists) { + res.status = 404; + res.set_content( + json{ + {"error", "Area not found"}, + {"area_id", area_id} + }.dump(2), + "application/json" + ); + return; + } + + // Filter components by area + json components_json = json::array(); + for (const auto& component : cache.components) { + if (component.area == area_id) { + components_json.push_back(component.to_json()); + } + } + + res.set_content(components_json.dump(2), "application/json"); + } catch (const std::exception& e) { + res.status = 500; + res.set_content( + json{{"error", "Internal server error"}}.dump(), + "application/json" + ); + RCLCPP_ERROR( + rclcpp::get_logger("rest_server"), + "Error in handle_area_components: %s", + e.what() + ); + } +} + } // namespace ros2_medkit_gateway diff --git a/src/ros2_medkit_gateway/test/test_integration.test.py b/src/ros2_medkit_gateway/test/test_integration.test.py index 988e0e1..0955067 100644 --- a/src/ros2_medkit_gateway/test/test_integration.test.py +++ b/src/ros2_medkit_gateway/test/test_integration.test.py @@ -212,3 +212,36 @@ def test_04_automotive_areas_discovery(self): self.assertIn(expected, area_ids) print(f'✓ All automotive areas discovered: {area_ids}') + + def test_05_area_components_success(self): + """Test GET /areas/{area_id}/components returns components for valid area.""" + # Test powertrain area + components = self._get_json('/areas/powertrain/components') + self.assertIsInstance(components, list) + self.assertGreater(len(components), 0) + + # All components should belong to powertrain area + for component in components: + self.assertEqual(component['area'], 'powertrain') + self.assertIn('id', component) + self.assertIn('namespace', component) + + # Verify expected powertrain components + component_ids = [comp['id'] for comp in components] + self.assertIn('temp_sensor', component_ids) + self.assertIn('rpm_sensor', component_ids) + + print(f'✓ Area components test passed: {len(components)} components in powertrain') + + def test_06_area_components_nonexistent_error(self): + """Test GET /areas/{area_id}/components returns 404 for nonexistent area.""" + response = requests.get(f'{self.BASE_URL}/areas/nonexistent/components', timeout=5) + self.assertEqual(response.status_code, 404) + + data = response.json() + self.assertIn('error', data) + self.assertEqual(data['error'], 'Area not found') + self.assertIn('area_id', data) + self.assertEqual(data['area_id'], 'nonexistent') + + print('✓ Nonexistent area error test passed')