Skip to content

Commit c2f9445

Browse files
author
Michał Fąferek
committed
[#34] fix: use C++23 std::expected and httplib status enums
- Upgrade to C++23 for std::expected support - Refactor validate_entity_id to return std::expected<void, std::string> - Replace magic numbers with httplib::StatusCode enums
1 parent 5866028 commit c2f9445

File tree

4 files changed

+64
-41
lines changed

4 files changed

+64
-41
lines changed

src/ros2_medkit_gateway/CMakeLists.txt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
cmake_minimum_required(VERSION 3.8)
22
project(ros2_medkit_gateway)
33

4-
# Compiler settings
5-
if(NOT CMAKE_CXX_STANDARD)
6-
set(CMAKE_CXX_STANDARD 17)
7-
endif()
4+
# Compiler settings - C++23 required for std::expected
5+
set(CMAKE_CXX_STANDARD 23)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
87

98
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
109
add_compile_options(-Wall -Wextra -Wpedantic)

src/ros2_medkit_gateway/include/ros2_medkit_gateway/rest_server.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <httplib.h>
1818

19+
#include <expected>
1920
#include <memory>
2021
#include <string>
2122

@@ -45,7 +46,8 @@ class RESTServer {
4546
void handle_component_data(const httplib::Request& req, httplib::Response& res);
4647

4748
// Helper methods
48-
bool validate_entity_id(const std::string& entity_id, std::string& error_message) const;
49+
// Returns success (void) or error message string
50+
std::expected<void, std::string> validate_entity_id(const std::string& entity_id) const;
4951

5052
GatewayNode* node_;
5153
std::string host_;

src/ros2_medkit_gateway/src/rest_server.cpp

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "ros2_medkit_gateway/gateway_node.hpp"
2020

2121
using json = nlohmann::json;
22+
using httplib::StatusCode;
2223

2324
namespace ros2_medkit_gateway {
2425

@@ -83,20 +84,17 @@ void RESTServer::stop() {
8384
}
8485
}
8586

86-
bool RESTServer::validate_entity_id(
87-
const std::string& entity_id,
88-
std::string& error_message
87+
std::expected<void, std::string> RESTServer::validate_entity_id(
88+
const std::string& entity_id
8989
) const {
9090
// Check for empty string
9191
if (entity_id.empty()) {
92-
error_message = "Entity ID cannot be empty";
93-
return false;
92+
return std::unexpected("Entity ID cannot be empty");
9493
}
9594

9695
// Check length (reasonable limit to prevent abuse)
9796
if (entity_id.length() > 256) {
98-
error_message = "Entity ID too long (max 256 characters)";
99-
return false;
97+
return std::unexpected("Entity ID too long (max 256 characters)");
10098
}
10199

102100
// Validate characters according to ROS 2 naming conventions
@@ -120,14 +118,14 @@ bool RESTServer::validate_entity_id(
120118
} else {
121119
char_repr = std::string(1, c);
122120
}
123-
error_message = "Entity ID contains invalid character: '" +
124-
char_repr +
125-
"'. Only alphanumeric and underscore are allowed";
126-
return false;
121+
return std::unexpected(
122+
"Entity ID contains invalid character: '" + char_repr +
123+
"'. Only alphanumeric and underscore are allowed"
124+
);
127125
}
128126
}
129127

130-
return true;
128+
return {};
131129
}
132130

133131
void RESTServer::handle_health(const httplib::Request& req, httplib::Response& res) {
@@ -141,7 +139,7 @@ void RESTServer::handle_health(const httplib::Request& req, httplib::Response& r
141139

142140
res.set_content(response.dump(2), "application/json");
143141
} catch (const std::exception& e) {
144-
res.status = 500;
142+
res.status = StatusCode::InternalServerError_500;
145143
res.set_content(
146144
json{{"error", "Internal server error"}}.dump(),
147145
"application/json"
@@ -162,7 +160,7 @@ void RESTServer::handle_root(const httplib::Request& req, httplib::Response& res
162160

163161
res.set_content(response.dump(2), "application/json");
164162
} catch (const std::exception& e) {
165-
res.status = 500;
163+
res.status = StatusCode::InternalServerError_500;
166164
res.set_content(
167165
json{{"error", "Internal server error"}}.dump(),
168166
"application/json"
@@ -184,7 +182,7 @@ void RESTServer::handle_list_areas(const httplib::Request& req, httplib::Respons
184182

185183
res.set_content(areas_json.dump(2), "application/json");
186184
} catch (const std::exception& e) {
187-
res.status = 500;
185+
res.status = StatusCode::InternalServerError_500;
188186
res.set_content(
189187
json{{"error", "Internal server error"}}.dump(),
190188
"application/json"
@@ -206,7 +204,7 @@ void RESTServer::handle_list_components(const httplib::Request& req, httplib::Re
206204

207205
res.set_content(components_json.dump(2), "application/json");
208206
} catch (const std::exception& e) {
209-
res.status = 500;
207+
res.status = StatusCode::InternalServerError_500;
210208
res.set_content(
211209
json{{"error", "Internal server error"}}.dump(),
212210
"application/json"
@@ -223,7 +221,7 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
223221
try {
224222
// Extract area_id from URL path
225223
if (req.matches.size() < 2) {
226-
res.status = 400;
224+
res.status = StatusCode::BadRequest_400;
227225
res.set_content(
228226
json{{"error", "Invalid request"}}.dump(2),
229227
"application/json"
@@ -234,13 +232,13 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
234232
std::string area_id = req.matches[1];
235233

236234
// Validate area_id
237-
std::string validation_error;
238-
if (!validate_entity_id(area_id, validation_error)) {
239-
res.status = 400;
235+
auto validation_result = validate_entity_id(area_id);
236+
if (!validation_result) {
237+
res.status = StatusCode::BadRequest_400;
240238
res.set_content(
241239
json{
242240
{"error", "Invalid area ID"},
243-
{"details", validation_error},
241+
{"details", validation_result.error()},
244242
{"area_id", area_id}
245243
}.dump(2),
246244
"application/json"
@@ -260,7 +258,7 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
260258
}
261259

262260
if (!area_exists) {
263-
res.status = 404;
261+
res.status = StatusCode::NotFound_404;
264262
res.set_content(
265263
json{
266264
{"error", "Area not found"},
@@ -281,7 +279,7 @@ void RESTServer::handle_area_components(const httplib::Request& req, httplib::Re
281279

282280
res.set_content(components_json.dump(2), "application/json");
283281
} catch (const std::exception& e) {
284-
res.status = 500;
282+
res.status = StatusCode::InternalServerError_500;
285283
res.set_content(
286284
json{{"error", "Internal server error"}}.dump(),
287285
"application/json"
@@ -299,7 +297,7 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
299297
try {
300298
// Extract component_id from URL path
301299
if (req.matches.size() < 2) {
302-
res.status = 400;
300+
res.status = StatusCode::BadRequest_400;
303301
res.set_content(
304302
json{{"error", "Invalid request"}}.dump(2),
305303
"application/json"
@@ -310,13 +308,13 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
310308
component_id = req.matches[1];
311309

312310
// Validate component_id
313-
std::string validation_error;
314-
if (!validate_entity_id(component_id, validation_error)) {
315-
res.status = 400;
311+
auto validation_result = validate_entity_id(component_id);
312+
if (!validation_result) {
313+
res.status = StatusCode::BadRequest_400;
316314
res.set_content(
317315
json{
318316
{"error", "Invalid component ID"},
319-
{"details", validation_error},
317+
{"details", validation_result.error()},
320318
{"component_id", component_id}
321319
}.dump(2),
322320
"application/json"
@@ -339,7 +337,7 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
339337
}
340338

341339
if (!component_found) {
342-
res.status = 404;
340+
res.status = StatusCode::NotFound_404;
343341
res.set_content(
344342
json{
345343
{"error", "Component not found"},
@@ -356,7 +354,7 @@ void RESTServer::handle_component_data(const httplib::Request& req, httplib::Res
356354

357355
res.set_content(component_data.dump(2), "application/json");
358356
} catch (const std::exception& e) {
359-
res.status = 500;
357+
res.status = StatusCode::InternalServerError_500;
360358
res.set_content(
361359
json{
362360
{"error", "Failed to retrieve component data"},

src/ros2_medkit_gateway/test/test_integration.test.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,11 @@ def test_03_list_components(self):
220220
print(f'✓ Components test passed: {len(components)} components discovered')
221221

222222
def test_04_automotive_areas_discovery(self):
223-
"""Test that automotive areas are properly discovered."""
223+
"""
224+
Test that automotive areas are properly discovered.
225+
226+
@verifies REQ_INTEROP_003
227+
"""
224228
areas = self._get_json('/areas')
225229
area_ids = [area['id'] for area in areas]
226230

@@ -257,7 +261,11 @@ def test_05_area_components_success(self):
257261
)
258262

259263
def test_06_area_components_nonexistent_error(self):
260-
"""Test GET /areas/{area_id}/components returns 404 for nonexistent area."""
264+
"""
265+
Test GET /areas/{area_id}/components returns 404 for nonexistent area.
266+
267+
@verifies REQ_INTEROP_006
268+
"""
261269
response = requests.get(
262270
f'{self.BASE_URL}/areas/nonexistent/components', timeout=5
263271
)
@@ -388,7 +396,11 @@ def test_12_component_no_topics(self):
388396
print(f'✓ Component with no topics test passed: {len(data)} topics')
389397

390398
def test_13_invalid_component_id_special_chars(self):
391-
"""Test GET /components/{component_id}/data rejects special characters."""
399+
"""
400+
Test GET /components/{component_id}/data rejects special characters.
401+
402+
@verifies REQ_INTEROP_018
403+
"""
392404
# Test various invalid characters
393405
invalid_ids = [
394406
'component;drop', # SQL injection attempt
@@ -419,7 +431,11 @@ def test_13_invalid_component_id_special_chars(self):
419431
print('✓ Invalid component ID special characters test passed')
420432

421433
def test_14_invalid_area_id_special_chars(self):
422-
"""Test GET /areas/{area_id}/components rejects special characters."""
434+
"""
435+
Test GET /areas/{area_id}/components rejects special characters.
436+
437+
@verifies REQ_INTEROP_006
438+
"""
423439
# Test various invalid characters
424440
# Note: Forward slash is handled by URL routing, not validation
425441
invalid_ids = [
@@ -448,7 +464,11 @@ def test_14_invalid_area_id_special_chars(self):
448464
print('✓ Invalid area ID special characters test passed')
449465

450466
def test_15_valid_ids_with_underscores(self):
451-
"""Test that valid IDs with underscores are accepted (ROS 2 naming)."""
467+
"""
468+
Test that valid IDs with underscores are accepted (ROS 2 naming).
469+
470+
@verifies REQ_INTEROP_018
471+
"""
452472
# While these IDs don't exist in the test environment,
453473
# they should pass validation and return 404 (not 400)
454474
valid_ids = [
@@ -473,7 +493,11 @@ def test_15_valid_ids_with_underscores(self):
473493
print('✓ Valid IDs with underscores test passed')
474494

475495
def test_16_invalid_ids_with_hyphens(self):
476-
"""Test that IDs with hyphens are rejected (not allowed in ROS 2 names)."""
496+
"""
497+
Test that IDs with hyphens are rejected (not allowed in ROS 2 names).
498+
499+
@verifies REQ_INTEROP_018
500+
"""
477501
invalid_ids = [
478502
'component-name',
479503
'component-name-123',

0 commit comments

Comments
 (0)