Skip to content

Commit c8eaa64

Browse files
author
Michał Fąferek
committed
[#16] feat: implement GET /areas/{area_id}/components endpoint
Add endpoint to list components within a specific area with error handling. Changes: - Add REST handler for GET /areas/{area_id}/components with path parameter - Implement area validation and 404 error response for nonexistent areas - Filter components by area from entity cache - Add test_05_area_components_success integration test - Add test_06_area_components_nonexistent_error integration test - Update README with API reference, success/error examples, and use cases - Update Postman collection with new endpoint and testing instructions
1 parent fd71fa9 commit c8eaa64

7 files changed

Lines changed: 175 additions & 1 deletion

File tree

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The `architecture.puml` file contains a PlantUML class diagram showing the relat
2020
- Extracts the entity hierarchy from the ROS 2 graph
2121

2222
3. **RESTServer** - Provides the HTTP/REST API
23-
- Serves endpoints: `/health`, `/`, `/areas`
23+
- Serves endpoints: `/health`, `/`, `/areas`, `/components`, `/areas/{area_id}/components`
2424
- Retrieves cached entities from the GatewayNode
2525
- Runs on configurable host and port
2626

postman/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Includes below endpoints:
1010
- ✅ GET `/` - Gateway info
1111
- ✅ GET `/areas` - List all areas
1212
- ✅ GET `/components` - List all components
13+
- ✅ GET `/areas/{area_id}/components` - List components in specific area
1314

1415
## Quick Start
1516

@@ -54,6 +55,10 @@ ros2 launch ros2_medkit_gateway demo_nodes.launch.py
5455
9. Click **Send**
5556
10. You should see components: `[{"id": "temp_sensor", "namespace": "/powertrain/engine", ...}, ...]`
5657

58+
11. Click **"GET Area Components"**
59+
12. Click **Send**
60+
13. You should see only powertrain components: `[{"id": "temp_sensor", "area": "powertrain", ...}, ...]`
61+
5762
## API Variables
5863

5964
The environment includes:

postman/collections/ros2-medkit-gateway.postman_collection.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@
6161
"description": "List all discovered components across all areas. Returns component metadata including id, namespace, fqn, type, and parent area."
6262
},
6363
"response": []
64+
},
65+
{
66+
"name": "GET Area Components",
67+
"request": {
68+
"method": "GET",
69+
"header": [],
70+
"url": {
71+
"raw": "{{base_url}}/areas/powertrain/components",
72+
"host": [
73+
"{{base_url}}"
74+
],
75+
"path": [
76+
"areas",
77+
"powertrain",
78+
"components"
79+
]
80+
},
81+
"description": "List components within a specific area. Returns 404 if area doesn't exist. Change 'powertrain' to other areas like 'chassis', 'body', etc."
82+
},
83+
"response": []
6484
}
6585
]
6686
}

src/ros2_medkit_gateway/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The ROS 2 Medkit Gateway exposes ROS 2 system information and data through a RES
2020
- `GET /` - Gateway status and version information
2121
- `GET /areas` - List all discovered areas (powertrain, chassis, body, root)
2222
- `GET /components` - List all discovered components across all areas
23+
- `GET /areas/{area_id}/components` - List components within a specific area
2324

2425
### API Reference
2526

@@ -84,6 +85,56 @@ curl http://localhost:8080/components
8485
- `type` - Always "Component"
8586
- `area` - Parent area this component belongs to
8687

88+
#### GET /areas/{area_id}/components
89+
90+
Lists all components within a specific area.
91+
92+
**Example (Success):**
93+
```bash
94+
curl http://localhost:8080/areas/powertrain/components
95+
```
96+
97+
**Response (200 OK):**
98+
```json
99+
[
100+
{
101+
"id": "temp_sensor",
102+
"namespace": "/powertrain/engine",
103+
"fqn": "/powertrain/engine/temp_sensor",
104+
"type": "Component",
105+
"area": "powertrain"
106+
},
107+
{
108+
"id": "rpm_sensor",
109+
"namespace": "/powertrain/engine",
110+
"fqn": "/powertrain/engine/rpm_sensor",
111+
"type": "Component",
112+
"area": "powertrain"
113+
}
114+
]
115+
```
116+
117+
**Example (Error - Area Not Found):**
118+
```bash
119+
curl http://localhost:8080/areas/nonexistent/components
120+
```
121+
122+
**Response (404 Not Found):**
123+
```json
124+
{
125+
"error": "Area not found",
126+
"area_id": "nonexistent"
127+
}
128+
```
129+
130+
**URL Parameters:**
131+
- `area_id` - Area identifier (e.g., `powertrain`, `chassis`, `body`)
132+
133+
**Use Cases:**
134+
- Filter components by domain (only show powertrain components)
135+
- Hierarchical navigation (select area → view its components)
136+
- Area-specific health checks
137+
87138
## Quick Start
88139

89140
### Build

src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class RESTServer {
4141
void handle_root(const httplib::Request& req, httplib::Response& res);
4242
void handle_list_areas(const httplib::Request& req, httplib::Response& res);
4343
void handle_list_components(const httplib::Request& req, httplib::Response& res);
44+
void handle_area_components(const httplib::Request& req, httplib::Response& res);
4445

4546
GatewayNode* node_;
4647
std::string host_;

src/ros2_medkit_gateway/src/rest_server.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ void RESTServer::setup_routes() {
5151
server_->Get("/components", [this](const httplib::Request& req, httplib::Response& res) {
5252
handle_list_components(req, res);
5353
});
54+
55+
// Area components
56+
server_->Get(R"(/areas/([^/]+)/components)", [this](const httplib::Request& req, httplib::Response& res) {
57+
handle_area_components(req, res);
58+
});
5459
}
5560

5661
void RESTServer::start() {
@@ -160,4 +165,63 @@ void RESTServer::handle_list_components(const httplib::Request& req, httplib::Re
160165
}
161166
}
162167

168+
void RESTServer::handle_area_components(const httplib::Request& req, httplib::Response& res) {
169+
try {
170+
// Extract area_id from URL path
171+
if (req.matches.size() < 2) {
172+
res.status = 400;
173+
res.set_content(
174+
json{{"error", "Invalid request"}}.dump(),
175+
"application/json"
176+
);
177+
return;
178+
}
179+
180+
std::string area_id = req.matches[1];
181+
const auto cache = node_->get_entity_cache();
182+
183+
// Check if area exists
184+
bool area_exists = false;
185+
for (const auto& area : cache.areas) {
186+
if (area.id == area_id) {
187+
area_exists = true;
188+
break;
189+
}
190+
}
191+
192+
if (!area_exists) {
193+
res.status = 404;
194+
res.set_content(
195+
json{
196+
{"error", "Area not found"},
197+
{"area_id", area_id}
198+
}.dump(2),
199+
"application/json"
200+
);
201+
return;
202+
}
203+
204+
// Filter components by area
205+
json components_json = json::array();
206+
for (const auto& component : cache.components) {
207+
if (component.area == area_id) {
208+
components_json.push_back(component.to_json());
209+
}
210+
}
211+
212+
res.set_content(components_json.dump(2), "application/json");
213+
} catch (const std::exception& e) {
214+
res.status = 500;
215+
res.set_content(
216+
json{{"error", "Internal server error"}}.dump(),
217+
"application/json"
218+
);
219+
RCLCPP_ERROR(
220+
rclcpp::get_logger("rest_server"),
221+
"Error in handle_area_components: %s",
222+
e.what()
223+
);
224+
}
225+
}
226+
163227
} // namespace ros2_medkit_gateway

src/ros2_medkit_gateway/test/test_integration.test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,36 @@ def test_04_automotive_areas_discovery(self):
212212
self.assertIn(expected, area_ids)
213213

214214
print(f'✓ All automotive areas discovered: {area_ids}')
215+
216+
def test_05_area_components_success(self):
217+
"""Test GET /areas/{area_id}/components returns components for valid area."""
218+
# Test powertrain area
219+
components = self._get_json('/areas/powertrain/components')
220+
self.assertIsInstance(components, list)
221+
self.assertGreater(len(components), 0)
222+
223+
# All components should belong to powertrain area
224+
for component in components:
225+
self.assertEqual(component['area'], 'powertrain')
226+
self.assertIn('id', component)
227+
self.assertIn('namespace', component)
228+
229+
# Verify expected powertrain components
230+
component_ids = [comp['id'] for comp in components]
231+
self.assertIn('temp_sensor', component_ids)
232+
self.assertIn('rpm_sensor', component_ids)
233+
234+
print(f'✓ Area components test passed: {len(components)} components in powertrain')
235+
236+
def test_06_area_components_nonexistent_error(self):
237+
"""Test GET /areas/{area_id}/components returns 404 for nonexistent area."""
238+
response = requests.get(f'{self.BASE_URL}/areas/nonexistent/components', timeout=5)
239+
self.assertEqual(response.status_code, 404)
240+
241+
data = response.json()
242+
self.assertIn('error', data)
243+
self.assertEqual(data['error'], 'Area not found')
244+
self.assertIn('area_id', data)
245+
self.assertEqual(data['area_id'], 'nonexistent')
246+
247+
print('✓ Nonexistent area error test passed')

0 commit comments

Comments
 (0)