diff --git a/src/rai_core/rai/tools/names.py b/src/rai_core/rai/tools/names.py new file mode 100644 index 000000000..c744834a0 --- /dev/null +++ b/src/rai_core/rai/tools/names.py @@ -0,0 +1,25 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""ROS name validation helpers shared by tools (stdlib-only).""" + + +def require_non_empty_name(value: object, *, name: str = "name") -> str: + """Return stripped name or raise ValueError if empty/non-string.""" + if not isinstance(value, str): + raise ValueError(f"{name} must be a non-empty string") + stripped = value.strip() + if not stripped: + raise ValueError(f"{name} must be a non-empty string") + return stripped diff --git a/src/rai_core/rai/tools/ros2/generic/topics.py b/src/rai_core/rai/tools/ros2/generic/topics.py index ad4a1cb67..ff8a45fa5 100644 --- a/src/rai_core/rai/tools/ros2/generic/topics.py +++ b/src/rai_core/rai/tools/ros2/generic/topics.py @@ -28,6 +28,7 @@ from rai.communication.ros2.api.conversion import ros2_message_to_dict from rai.messages import MultimodalArtifact, preprocess_image from rai.tools.ros2.base import BaseROS2Tool, BaseROS2Toolkit +from rai.tools.names import require_non_empty_name from rai.tools.ros2.generic.interface_parser import render_interface_string @@ -264,6 +265,8 @@ class GetROS2TransformTool(BaseROS2Tool): STALE_TRANSFORM_THRESHOLD_SEC: float = 1.0 def _run(self, target_frame: str, source_frame: str, timeout_sec: float) -> str: + target_frame = require_non_empty_name(target_frame, name="target_frame") + source_frame = require_non_empty_name(source_frame, name="source_frame") transform = self.connector.get_transform( target_frame=target_frame, source_frame=source_frame, diff --git a/tests/tools/test_require_non_empty_name.py b/tests/tools/test_require_non_empty_name.py new file mode 100644 index 000000000..c04294b30 --- /dev/null +++ b/tests/tools/test_require_non_empty_name.py @@ -0,0 +1,27 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from rai.tools.names import require_non_empty_name + + +@pytest.mark.parametrize("value", ["", " ", None, 1, True]) +def test_require_non_empty_name_rejects(value): + with pytest.raises(ValueError, match="non-empty"): + require_non_empty_name(value, name="topic") + + +def test_require_non_empty_name_strips(): + assert require_non_empty_name(" /chatter ", name="topic") == "/chatter"