From e337349f6ee1256d665d293c71856eafcb9907cd Mon Sep 17 00:00:00 2001 From: Maximilian Schmidt Date: Tue, 23 Jun 2026 19:17:30 +0200 Subject: [PATCH] refactor(ros-z): redesign endpoint builders --- crates/nodes/active_vision/src/lib.rs | 14 +- crates/nodes/ball_filter/src/lib.rs | 20 +- crates/nodes/ball_state_composer/src/lib.rs | 25 +- crates/nodes/behavior_node/src/node.rs | 61 ++- crates/nodes/booster_sdk_interface/src/lib.rs | 6 +- crates/nodes/button_event_bridge/src/lib.rs | 2 +- crates/nodes/button_event_handler/src/lib.rs | 4 +- .../nodes/camera_matrix_calculator/src/lib.rs | 10 +- crates/nodes/detection/src/lib.rs | 8 +- crates/nodes/fake_odometry/src/lib.rs | 2 +- .../nodes/fall_down_state_receiver/src/lib.rs | 2 +- .../nodes/field_border_detection/src/lib.rs | 9 +- .../nodes/game_controller_filter/src/lib.rs | 8 +- .../game_controller_state_filter/src/lib.rs | 22 +- .../global_parameter_provider/src/lib.rs | 4 +- crates/nodes/ground_provider/src/lib.rs | 16 +- crates/nodes/head_motion/src/lib.rs | 10 +- crates/nodes/image_receiver/src/lib.rs | 10 +- crates/nodes/image_segmenter/src/lib.rs | 7 +- crates/nodes/kick/src/lib.rs | 4 +- crates/nodes/kinematics_provider/src/lib.rs | 4 +- crates/nodes/led_handler/src/lib.rs | 4 +- crates/nodes/line_detection/src/lib.rs | 16 +- crates/nodes/localization/src/lib.rs | 32 +- crates/nodes/look_around/src/lib.rs | 8 +- crates/nodes/look_at/src/lib.rs | 13 +- crates/nodes/low_command_publisher/src/lib.rs | 4 +- crates/nodes/low_state_bridge/src/lib.rs | 8 +- crates/nodes/message_filter/src/lib.rs | 6 +- crates/nodes/message_handler/src/lib.rs | 4 +- crates/nodes/microphone_recorder/src/lib.rs | 2 +- .../nodes/motor_commands_collector/src/lib.rs | 4 +- crates/nodes/obstacle_filter/src/lib.rs | 36 +- crates/nodes/player_state_receiver/src/lib.rs | 6 +- crates/nodes/primary_state_filter/src/lib.rs | 14 +- crates/nodes/robot_mode_handler/src/lib.rs | 8 +- crates/nodes/rotate_head/src/lib.rs | 4 +- .../nodes/rule_obstacle_composer/src/lib.rs | 12 +- crates/nodes/safe_pose_checker/src/lib.rs | 15 +- crates/nodes/search_suggestor/src/lib.rs | 22 +- crates/nodes/segment_filter/src/lib.rs | 7 +- crates/nodes/stand_up/src/lib.rs | 4 +- .../nodes/support_foot_estimator/src/lib.rs | 10 +- crates/nodes/team_ball_receiver/src/lib.rs | 8 +- .../time_to_reach_kick_position/src/lib.rs | 4 +- crates/nodes/walking/src/lib.rs | 6 +- crates/nodes/whistle_detection/src/lib.rs | 6 +- crates/nodes/whistle_filter/src/lib.rs | 4 +- crates/nodes/world_state_composer/src/lib.rs | 26 +- .../nodes/world_to_field_provider/src/lib.rs | 4 +- crates/ros-z-cli/src/app/context.rs | 14 +- crates/ros-z-cli/src/commands/echo.rs | 11 +- crates/ros-z-cli/src/commands/schema.rs | 2 +- crates/ros-z-cli/tests/e2e.rs | 11 +- crates/ros-z-debug/src/manager.rs | 3 +- crates/ros-z-debug/tests/integration.rs | 3 - crates/ros-z-streams/src/announce.rs | 7 +- crates/ros-z-streams/src/future_queue.rs | 4 +- crates/ros-z/README.md | 30 +- .../custom_message/navigation_client.rs | 2 +- .../custom_message/navigation_server.rs | 2 +- .../custom_message/status_publisher.rs | 2 +- .../custom_message/status_subscriber.rs | 2 +- crates/ros-z/examples/pubsub/listener.rs | 2 +- crates/ros-z/examples/pubsub/talker.rs | 2 +- crates/ros-z/examples/service/client.rs | 2 +- crates/ros-z/examples/service/server.rs | 2 +- crates/ros-z/src/cache.rs | 56 ++- crates/ros-z/src/dynamic/discovery.rs | 202 ++++++++- crates/ros-z/src/dynamic/error.rs | 2 +- crates/ros-z/src/dynamic/mod.rs | 4 +- crates/ros-z/src/dynamic/registry.rs | 15 +- crates/ros-z/src/dynamic/schema_query.rs | 19 +- crates/ros-z/src/dynamic/schema_service.rs | 82 ++-- crates/ros-z/src/endpoint_builder.rs | 310 ++++++++++++++ crates/ros-z/src/error.rs | 13 - crates/ros-z/src/lib.rs | 13 +- crates/ros-z/src/node.rs | 404 ++++++------------ crates/ros-z/src/parameter/remote/client.rs | 4 +- crates/ros-z/src/parameter/remote/services.rs | 8 +- crates/ros-z/src/pubsub.rs | 1 + crates/ros-z/src/pubsub/publisher.rs | 202 +++++---- crates/ros-z/src/pubsub/subscriber.rs | 230 +++++++--- crates/ros-z/src/service.rs | 226 ++++++---- crates/ros-z/src/shm.rs | 2 +- .../tests/communication_pubsub_alignment.rs | 4 - crates/ros-z/tests/endpoint_builder_api.rs | 6 + crates/ros-z/tests/extended_type_info.rs | 27 +- crates/ros-z/tests/graph.rs | 54 +-- .../ros-z/tests/nalgebra_field_type_info.rs | 10 +- crates/ros-z/tests/parameter_integration.rs | 14 +- crates/ros-z/tests/pubsub.rs | 182 ++++++-- crates/ros-z/tests/pubsub_qos.rs | 121 ++---- crates/ros-z/tests/service.rs | 125 +++--- crates/ros-z/tests/shm.rs | 16 - .../dynamic_subscriber_cache.rs | 35 ++ .../dynamic_subscriber_cache.stderr | 18 + .../tests/ui/endpoint_builder/new_api_pass.rs | 134 ++++++ 98 files changed, 1976 insertions(+), 1209 deletions(-) create mode 100644 crates/ros-z/src/endpoint_builder.rs create mode 100644 crates/ros-z/tests/endpoint_builder_api.rs create mode 100644 crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.rs create mode 100644 crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.stderr create mode 100644 crates/ros-z/tests/ui/endpoint_builder/new_api_pass.rs diff --git a/crates/nodes/active_vision/src/lib.rs b/crates/nodes/active_vision/src/lib.rs index 83dff8bf49..9f386f01bc 100644 --- a/crates/nodes/active_vision/src/lib.rs +++ b/crates/nodes/active_vision/src/lib.rs @@ -19,32 +19,32 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("active_vision")?; let _field_dimensions_sub = node - .subscriber::("field_dimensions")? + .subscriber::("field_dimensions") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) .build() .await?; - let _ball_sub = node.subscriber::("ball_state")?.build().await?; + let _ball_sub = node.subscriber::("ball_state").build().await?; let _rule_ball_sub = node - .subscriber::("rule_ball_state")? + .subscriber::("rule_ball_state") .build() .await?; let _obstacles_sub = node - .subscriber::>("obstacles")? + .subscriber::>("obstacles") .build() .await?; let _ground_to_field_sub = node - .subscriber::>("ground_to_field")? + .subscriber::>("ground_to_field") .build() .await?; let _filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let _position_of_interest_pub = node - .publisher::>("position_of_interest")? + .publisher::>("position_of_interest") .build() .await?; diff --git a/crates/nodes/ball_filter/src/lib.rs b/crates/nodes/ball_filter/src/lib.rs index f29785d3e6..e571daed40 100644 --- a/crates/nodes/ball_filter/src/lib.rs +++ b/crates/nodes/ball_filter/src/lib.rs @@ -41,15 +41,17 @@ pub async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("ball_filter")?; let field_dimensions_sub = node - .create_cache::("field_dimensions", 1)? - .with_qos(QosProfile { + .subscriber::("field_dimensions") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let camera_matrix_cache = node - .create_cache::>("camera_matrix", 10)? + .subscriber::>("camera_matrix") + .cache(10) .with_stamp(|wrapper: &TimeWrapper| wrapper.time) .build() .await?; @@ -64,29 +66,29 @@ pub async fn run(ctx: Arc) -> Result<()> { .await? .build(); let filter_state_pub = node - .publisher::("ball_filter/ball_filter_state")? + .publisher::("ball_filter/ball_filter_state") .build() .await?; let best_ball_hypothesis_pub = node - .publisher::>("ball_filter/best_ball_hypothesis")? + .publisher::>("ball_filter/best_ball_hypothesis") .build() .await?; let filtered_balls_in_image_pub = node - .publisher::>>("ball_filter/filtered_balls_in_image")? + .publisher::>>("ball_filter/filtered_balls_in_image") .build() .await?; let ball_percepts_pub = node - .publisher::>("ball_filter/ball_percepts")? + .publisher::>("ball_filter/ball_percepts") .build() .await?; let ball_position_pub = node - .publisher::>>("ball_filter/ball_position")? + .publisher::>>("ball_filter/ball_position") .build() .await?; let hypothetical_ball_positions_pub = node .publisher::>>( "ball_filter/hypothetical_ball_positions", - )? + ) .build() .await?; diff --git a/crates/nodes/ball_state_composer/src/lib.rs b/crates/nodes/ball_state_composer/src/lib.rs index 101e480c77..439776b322 100644 --- a/crates/nodes/ball_state_composer/src/lib.rs +++ b/crates/nodes/ball_state_composer/src/lib.rs @@ -24,47 +24,50 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("ball_state_composer").build().await?; let field_dimensions_cache = node - .create_cache::("field_dimensions", 1)? - .with_qos(QosProfile { + .subscriber::("field_dimensions") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let ball_position_sub = node - .subscriber::>>("ball_filter/ball_position")? + .subscriber::>>("ball_filter/ball_position") .build() .await?; let ground_to_field_cache = node - .create_cache::>("ground_to_field", 10)? + .subscriber::>("ground_to_field") + .cache(10) .build() .await?; let team_ball_sub = node - .subscriber::>("team_ball")? + .subscriber::>("team_ball") .build() .await?; let primary_state_cache = node - .create_cache::("primary_state", 10)? - .with_qos(QosProfile { + .subscriber::("primary_state") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(10) .build() .await?; let filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let additional_last_ball_state_pub = node - .publisher::>("last_ball_state")? + .publisher::>("last_ball_state") .build() .await?; let ball_state_pub = node - .publisher::>("ball_state")? + .publisher::>("ball_state") .build() .await?; let rule_ball_state_pub = node - .publisher::>("rule_ball_state")? + .publisher::>("rule_ball_state") .build() .await?; diff --git a/crates/nodes/behavior_node/src/node.rs b/crates/nodes/behavior_node/src/node.rs index d047593792..866c2acc34 100644 --- a/crates/nodes/behavior_node/src/node.rs +++ b/crates/nodes/behavior_node/src/node.rs @@ -127,84 +127,99 @@ pub async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("behavior_node")?; parameters.add_validation_hook(validate_behavior_parameters)?; let field_dimensions_cache = node - .create_cache::("field_dimensions", 1)? - .with_qos(QosProfile { + .subscriber::("field_dimensions") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let player_number_cache = node - .create_cache::("player_number", 1)? - .with_qos(QosProfile { + .subscriber::("player_number") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let player_states_cache = node - .create_cache::>>>("player_states", 1)? + .subscriber::>>>("player_states") + .cache(1) .build() .await?; let fall_down_state_cache = node - .create_cache::("inputs/fall_down_state", 1)? + .subscriber::("inputs/fall_down_state") + .cache(1) .build() .await?; let ball_state_cache = node - .create_cache::>("ball_state", 1)? + .subscriber::>("ball_state") + .cache(1) .build() .await?; let filtered_game_controller_state_cache = node - .create_cache::("filtered_game_controller_state", 1)? + .subscriber::("filtered_game_controller_state") + .cache(1) .build() .await?; let game_controller_address_cache = node - .create_cache::>("game_controller_address", 1)? + .subscriber::>("game_controller_address") + .cache(1) .build() .await?; let ground_to_field_cache = node - .create_cache::>("ground_to_field", 1)? + .subscriber::>("ground_to_field") + .cache(1) .build() .await?; let hypothetical_ball_positions_cache = node - .create_cache::>>("hypothetical_ball_positions", 1)? + .subscriber::>>("hypothetical_ball_positions") + .cache(1) .build() .await?; let obstacles_cache = node - .create_cache::>("obstacles", 1)? + .subscriber::>("obstacles") + .cache(1) .build() .await?; let position_of_interest_cache = node - .create_cache::>("position_of_interest", 1)? + .subscriber::>("position_of_interest") + .cache(1) .build() .await?; let primary_state_cache = node - .create_cache::("primary_state", 1)? - .with_qos(QosProfile { + .subscriber::("primary_state") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let rule_ball_cache = node - .create_cache::>("rule_ball_state", 1)? + .subscriber::>("rule_ball_state") + .cache(1) .build() .await?; let rule_obstacles_cache = node - .create_cache::>("rule_obstacles", 1)? + .subscriber::>("rule_obstacles") + .cache(1) .build() .await?; let suggested_search_position_cache = node - .create_cache::>("suggested_search_position", 1)? + .subscriber::>("suggested_search_position") + .cache(1) .build() .await?; let additional_behavior_trace_pub = node - .publisher::("behavior/trace")? + .publisher::("behavior/trace") .build() .await?; let additional_behavior_tree_layout_pub = node - .publisher::("behavior/tree_layout")? + .publisher::("behavior/tree_layout") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -212,15 +227,15 @@ pub async fn run(ctx: Arc) -> Result<()> { .build() .await?; let additional_black_board_pub = node - .publisher::("behavior/blackboard")? + .publisher::("behavior/blackboard") .build() .await?; let outgoing_message_pub = node - .publisher::("outputs/message")? + .publisher::("outputs/message") .build() .await?; let motion_command_pub = node - .publisher::("behavior/motion_command")? + .publisher::("behavior/motion_command") .build() .await?; diff --git a/crates/nodes/booster_sdk_interface/src/lib.rs b/crates/nodes/booster_sdk_interface/src/lib.rs index 6be3f5da83..03c99cc089 100644 --- a/crates/nodes/booster_sdk_interface/src/lib.rs +++ b/crates/nodes/booster_sdk_interface/src/lib.rs @@ -138,16 +138,16 @@ async fn run(ctx: Arc) -> Result<()> { let light_control_client = Arc::new(LightControlClient::new()?); let led_command_sub = node - .subscriber::("commands/led_command")? + .subscriber::("commands/led_command") .build() .await?; let high_level_command_sub = node - .subscriber::("commands/high_level_command")? + .subscriber::("commands/high_level_command") .build() .await?; let mut robot_mode_service: ServiceServer = node - .create_service_server::("services/get_robot_mode")? + .service_server::("services/get_robot_mode") .build() .await?; diff --git a/crates/nodes/button_event_bridge/src/lib.rs b/crates/nodes/button_event_bridge/src/lib.rs index 2117b3b22b..633327f98a 100644 --- a/crates/nodes/button_event_bridge/src/lib.rs +++ b/crates/nodes/button_event_bridge/src/lib.rs @@ -23,7 +23,7 @@ async fn run(ctx: Arc) -> Result<()> { .await .map_err(|error| color_eyre::eyre::eyre!("{error}"))?; let button_event_message_pub = node - .publisher::("inputs/button_event_message")? + .publisher::("inputs/button_event_message") .build() .await?; diff --git a/crates/nodes/button_event_handler/src/lib.rs b/crates/nodes/button_event_handler/src/lib.rs index 6762698200..0554d65b8f 100644 --- a/crates/nodes/button_event_handler/src/lib.rs +++ b/crates/nodes/button_event_handler/src/lib.rs @@ -14,11 +14,11 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("button_event_handler").build().await?; let button_event_message_sub = node - .subscriber::("inputs/button_event_message")? + .subscriber::("inputs/button_event_message") .build() .await?; let buttons_pub = node - .publisher::>>("buttons")? + .publisher::>>("buttons") .build() .await?; diff --git a/crates/nodes/camera_matrix_calculator/src/lib.rs b/crates/nodes/camera_matrix_calculator/src/lib.rs index d7cbb463a8..176fe3b5a1 100644 --- a/crates/nodes/camera_matrix_calculator/src/lib.rs +++ b/crates/nodes/camera_matrix_calculator/src/lib.rs @@ -21,21 +21,23 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("camera_matrix_calculator")?; let robot_kinematics_cache = node - .create_cache::>("robot_kinematics", 10)? + .subscriber::>("robot_kinematics") + .cache(10) .with_stamp(|w: &TimeWrapper| w.time) .build() .await?; let robot_to_ground_sub = node - .subscriber::>>>("robot_to_ground")? + .subscriber::>>>("robot_to_ground") .build() .await?; let camera_info_cache = node - .create_cache::("inputs/camera_info", 1)? + .subscriber::("inputs/camera_info") + .cache(1) .build() .await?; let camera_matrix_pub = node - .publisher::>("camera_matrix")? + .publisher::>("camera_matrix") .build() .await?; diff --git a/crates/nodes/detection/src/lib.rs b/crates/nodes/detection/src/lib.rs index 62dea81fb4..bce31b12d6 100644 --- a/crates/nodes/detection/src/lib.rs +++ b/crates/nodes/detection/src/lib.rs @@ -61,19 +61,19 @@ async fn run(ctx: Arc) -> Result<()> { let node_parameters = node.bind_parameter_as::("detection")?; let image_sub = node - .subscriber::>("inputs/left_image")? + .subscriber::>("inputs/left_image") .build() .await?; let inference_duration_pub = node - .publisher::("inference_duration")? + .publisher::("inference_duration") .build() .await?; let post_processing_duration_pub = node - .publisher::("post_processing_duration")? + .publisher::("post_processing_duration") .build() .await?; let non_maximum_suppression_duration_pub = node - .publisher::("non_maximum_suppression_duration")? + .publisher::("non_maximum_suppression_duration") .build() .await?; let detected_objects_pub = node diff --git a/crates/nodes/fake_odometry/src/lib.rs b/crates/nodes/fake_odometry/src/lib.rs index ce47ed916a..f9ca0a5920 100644 --- a/crates/nodes/fake_odometry/src/lib.rs +++ b/crates/nodes/fake_odometry/src/lib.rs @@ -13,7 +13,7 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("fake_odometry").build().await?; let _current_odometry_to_last_odometry_pub = node - .publisher::>("current_odometry_to_last_odometry")? + .publisher::>("current_odometry_to_last_odometry") .build() .await?; diff --git a/crates/nodes/fall_down_state_receiver/src/lib.rs b/crates/nodes/fall_down_state_receiver/src/lib.rs index b39b6b1e17..f6d4187e13 100644 --- a/crates/nodes/fall_down_state_receiver/src/lib.rs +++ b/crates/nodes/fall_down_state_receiver/src/lib.rs @@ -23,7 +23,7 @@ async fn run(ctx: Arc) -> Result<()> { .await .map_err(|error| eyre!("{error}"))?; let fall_down_state_pub = node - .publisher::("inputs/fall_down_state")? + .publisher::("inputs/fall_down_state") .build() .await?; diff --git a/crates/nodes/field_border_detection/src/lib.rs b/crates/nodes/field_border_detection/src/lib.rs index e2c72ec433..ba3fee0cc9 100644 --- a/crates/nodes/field_border_detection/src/lib.rs +++ b/crates/nodes/field_border_detection/src/lib.rs @@ -29,20 +29,21 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("field_border_detection")?; let camera_matrix_cache = node - .create_cache::>("camera_matrix", 10)? + .subscriber::>("camera_matrix") + .cache(10) .with_stamp(|w: &TimeWrapper| w.time) .build() .await?; let image_segments_sub = node - .subscriber::>("image_segments")? + .subscriber::>("image_segments") .build() .await?; let field_border_points_pub = node - .publisher::>>("field_border_points")? + .publisher::>>("field_border_points") .build() .await?; let field_border_pub = node - .publisher::>>("field_border")? + .publisher::>>("field_border") .build() .await?; diff --git a/crates/nodes/game_controller_filter/src/lib.rs b/crates/nodes/game_controller_filter/src/lib.rs index 6a02200180..c2f2925526 100644 --- a/crates/nodes/game_controller_filter/src/lib.rs +++ b/crates/nodes/game_controller_filter/src/lib.rs @@ -27,19 +27,19 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("game_controller_filter")?; let network_message_sub = node - .subscriber::>("filtered_message")? + .subscriber::>("filtered_message") .build() .await?; let last_contact_pub = node - .publisher::>("game_controller_address_contacts_times")? + .publisher::>("game_controller_address_contacts_times") .build() .await?; let game_controller_state_pub = node - .publisher::>("game_controller_state")? + .publisher::>("game_controller_state") .build() .await?; let game_controller_address_pub = node - .publisher::>("game_controller_address")? + .publisher::>("game_controller_address") .build() .await?; diff --git a/crates/nodes/game_controller_state_filter/src/lib.rs b/crates/nodes/game_controller_state_filter/src/lib.rs index a0926ff1fd..93614e77b6 100644 --- a/crates/nodes/game_controller_state_filter/src/lib.rs +++ b/crates/nodes/game_controller_state_filter/src/lib.rs @@ -32,41 +32,45 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("game_controller_state_filter")?; let field_dimensions_cache = node - .create_cache::("field_dimensions", 1)? - .with_qos(QosProfile { + .subscriber::("field_dimensions") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let player_number_cache = node - .create_cache::("player_number", 1)? - .with_qos(QosProfile { + .subscriber::("player_number") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let game_controller_state_sub = node - .subscriber::>("game_controller_state")? + .subscriber::>("game_controller_state") .build() .await?; let filtered_whistle_cache = node - .create_cache::("filtered_whistle", 1)? + .subscriber::("filtered_whistle") + .cache(1) .build() .await?; let ball_state_cache = node - .create_cache::>("ball_state", 1)? + .subscriber::>("ball_state") + .cache(1) .build() .await?; let whistle_in_set_ball_position_pub = node - .publisher::>>("whistle_in_set_ball_position")? + .publisher::>>("whistle_in_set_ball_position") .build() .await?; let filtered_game_controller_state_pub = node - .publisher::("filtered_game_controller_state")? + .publisher::("filtered_game_controller_state") .build() .await?; diff --git a/crates/nodes/global_parameter_provider/src/lib.rs b/crates/nodes/global_parameter_provider/src/lib.rs index c01fdef15c..610c7c621a 100644 --- a/crates/nodes/global_parameter_provider/src/lib.rs +++ b/crates/nodes/global_parameter_provider/src/lib.rs @@ -24,7 +24,7 @@ async fn run(ctx: Arc) -> Result<()> { let node_parameters = node.bind_parameter_as::("global")?; let player_number_pub = node - .publisher::("player_number")? + .publisher::("player_number") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -33,7 +33,7 @@ async fn run(ctx: Arc) -> Result<()> { .await?; let field_dimensions_pub = node - .publisher::("field_dimensions")? + .publisher::("field_dimensions") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() diff --git a/crates/nodes/ground_provider/src/lib.rs b/crates/nodes/ground_provider/src/lib.rs index da1174cf3a..e3cac2c47d 100644 --- a/crates/nodes/ground_provider/src/lib.rs +++ b/crates/nodes/ground_provider/src/lib.rs @@ -18,31 +18,33 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("ground_provider").build().await?; let imu_state_sub = node - .subscriber::("inputs/imu_state")? + .subscriber::("inputs/imu_state") .build() .await?; let robot_kinematics_cache = node - .create_cache::>("robot_kinematics", 10)? + .subscriber::>("robot_kinematics") + .cache(10) .with_stamp(|wrapper| wrapper.time) .build() .await?; let support_foot_cache = node - .create_cache::>>("support_foot", 10)? - .with_stamp(|wrapper| wrapper.time) - .with_qos(QosProfile { + .subscriber::>>("support_foot") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(10) + .with_stamp(|wrapper| wrapper.time) .build() .await?; let robot_to_ground_pub = node - .publisher::>>>("robot_to_ground")? + .publisher::>>>("robot_to_ground") .build() .await?; let ground_to_robot_pub = node - .publisher::>>>("ground_to_robot")? + .publisher::>>>("ground_to_robot") .build() .await?; diff --git a/crates/nodes/head_motion/src/lib.rs b/crates/nodes/head_motion/src/lib.rs index d31292fed1..a08517364c 100644 --- a/crates/nodes/head_motion/src/lib.rs +++ b/crates/nodes/head_motion/src/lib.rs @@ -24,23 +24,23 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("head_motion")?; let _look_around_target_joints_sub = node - .subscriber::>("look_around_target_joints")? + .subscriber::>("look_around_target_joints") .build() .await?; let _look_at_sub = node - .subscriber::>("look_at")? + .subscriber::>("look_at") .build() .await?; let _motor_states_sub = node - .subscriber::>("inputs/serial_motor_states")? + .subscriber::>("inputs/serial_motor_states") .build() .await?; let _motion_command_sub = node - .subscriber::("motion_command")? + .subscriber::("motion_command") .build() .await?; let _head_joints_command_pub = node - .publisher::>("head_joints_command")? + .publisher::>("head_joints_command") .build() .await?; diff --git a/crates/nodes/image_receiver/src/lib.rs b/crates/nodes/image_receiver/src/lib.rs index d6378696ba..133859521c 100644 --- a/crates/nodes/image_receiver/src/lib.rs +++ b/crates/nodes/image_receiver/src/lib.rs @@ -26,15 +26,15 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("image_receiver").build().await?; let left_image_pub = node - .publisher::>("inputs/left_image")? + .publisher::>("inputs/left_image") .build() .await?; let right_image_pub = node - .publisher::>("inputs/right_image")? + .publisher::>("inputs/right_image") .build() .await?; let camera_info_pub = node - .publisher::("inputs/camera_info")? + .publisher::("inputs/camera_info") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -42,11 +42,11 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let ycbcr422_image_pub = node - .publisher::>("inputs/ycbcr422_image")? + .publisher::>("inputs/ycbcr422_image") .build() .await?; let stereo_image_pair_pub = node - .publisher::>("inputs/stereo_image_pair")? + .publisher::>("inputs/stereo_image_pair") .build() .await?; diff --git a/crates/nodes/image_segmenter/src/lib.rs b/crates/nodes/image_segmenter/src/lib.rs index 2209fd79e2..1033b566be 100644 --- a/crates/nodes/image_segmenter/src/lib.rs +++ b/crates/nodes/image_segmenter/src/lib.rs @@ -31,16 +31,17 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("image_segmenter")?; let image_sub = node - .subscriber::>("inputs/ycbcr422_image")? + .subscriber::>("inputs/ycbcr422_image") .build() .await?; let camera_matrix_cache = node - .create_cache::>("camera_matrix", 10)? + .subscriber::>("camera_matrix") + .cache(10) .with_stamp(|w: &TimeWrapper| w.time) .build() .await?; let image_segments_pub = node - .publisher::>("image_segments")? + .publisher::>("image_segments") .build() .await?; diff --git a/crates/nodes/kick/src/lib.rs b/crates/nodes/kick/src/lib.rs index 3f833aaf36..24b848b95a 100644 --- a/crates/nodes/kick/src/lib.rs +++ b/crates/nodes/kick/src/lib.rs @@ -16,11 +16,11 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("kick")?; let _get_robot_mode_client = node - .create_service_client::("services/get_robot_mode")? + .service_client::("services/get_robot_mode") .build() .await?; let _motion_command_sub = node - .subscriber::("motion_command")? + .subscriber::("motion_command") .build() .await?; diff --git a/crates/nodes/kinematics_provider/src/lib.rs b/crates/nodes/kinematics_provider/src/lib.rs index 18c555212c..698c11cd04 100644 --- a/crates/nodes/kinematics_provider/src/lib.rs +++ b/crates/nodes/kinematics_provider/src/lib.rs @@ -34,11 +34,11 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("kinematics_provider").build().await?; let serial_motor_states_sub = node - .subscriber::>("inputs/serial_motor_states")? + .subscriber::>("inputs/serial_motor_states") .build() .await?; let robot_kinematics_pub = node - .publisher::>("robot_kinematics")? + .publisher::>("robot_kinematics") .build() .await?; diff --git a/crates/nodes/led_handler/src/lib.rs b/crates/nodes/led_handler/src/lib.rs index 00f2f3c0ef..7622995c7c 100644 --- a/crates/nodes/led_handler/src/lib.rs +++ b/crates/nodes/led_handler/src/lib.rs @@ -15,7 +15,7 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("led_handler").build().await?; let primary_state_sub = node - .subscriber::("primary_state")? + .subscriber::("primary_state") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -24,7 +24,7 @@ async fn run(ctx: Arc) -> Result<()> { .await?; let led_command_pub = node - .publisher::("commands/led_command")? + .publisher::("commands/led_command") .build() .await?; diff --git a/crates/nodes/line_detection/src/lib.rs b/crates/nodes/line_detection/src/lib.rs index bcbca2579c..00301e8f0c 100644 --- a/crates/nodes/line_detection/src/lib.rs +++ b/crates/nodes/line_detection/src/lib.rs @@ -39,33 +39,35 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("line_detection")?; let camera_matrix_cache = node - .create_cache::>("camera_matrix", 10)? + .subscriber::>("camera_matrix") + .cache(10) .with_stamp(|w: &TimeWrapper| w.time) .build() .await?; let filtered_segments_sub = node - .subscriber::>("filtered_segments")? + .subscriber::>("filtered_segments") .build() .await?; let image_cache = node - .create_cache::>("inputs/ycbcr422_image", 10)? + .subscriber::>("inputs/ycbcr422_image") + .cache(10) .with_stamp(|w: &TimeWrapper| w.time) .build() .await?; let lines_in_image_pub = node - .publisher::>>("line_detection/lines_in_image")? + .publisher::>>("line_detection/lines_in_image") .build() .await?; let discarded_lines_pub = node - .publisher::>("line_detection/discarded_lines")? + .publisher::>("line_detection/discarded_lines") .build() .await?; let filtered_segments_output_pub = node - .publisher::>("line_detection/filtered_segments")? + .publisher::>("line_detection/filtered_segments") .build() .await?; let line_data_pub = node - .publisher::>>("line_detection/lines_in_image")? + .publisher::>>("line_detection/lines_in_image") .build() .await?; diff --git a/crates/nodes/localization/src/lib.rs b/crates/nodes/localization/src/lib.rs index fe55e61e84..bbc2499068 100644 --- a/crates/nodes/localization/src/lib.rs +++ b/crates/nodes/localization/src/lib.rs @@ -58,11 +58,11 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("localization")?; let _filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let _primary_state_sub = node - .subscriber::("primary_state")? + .subscriber::("primary_state") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -70,20 +70,20 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let _odometer_sub = node - .subscriber::("inputs/odometer")? + .subscriber::("inputs/odometer") .build() .await?; let _fall_down_state_sub = node - .subscriber::("inputs/fall_down_state")? + .subscriber::("inputs/fall_down_state") .build() .await?; let _imu_state_sub = node - .subscriber::("inputs/imu_state")? + .subscriber::("inputs/imu_state") .build() .await?; - let _line_data_sub = node.subscriber::("line_data")?.build().await?; + let _line_data_sub = node.subscriber::("line_data").build().await?; let _field_dimensions_sub = node - .subscriber::("field_dimensions")? + .subscriber::("field_dimensions") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -91,7 +91,7 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let _player_number_sub = node - .subscriber::("player_number")? + .subscriber::("player_number") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -99,35 +99,35 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let _correspondence_lines_pub = node - .publisher::>>("localization/correspondence_lines")? + .publisher::>>("localization/correspondence_lines") .build() .await?; let _fit_errors_pub = node - .publisher::>>>>("localization/fit_errors")? + .publisher::>>>>("localization/fit_errors") .build() .await?; let _measured_lines_in_field_pub = node - .publisher::>>("localization/measured_lines_in_field")? + .publisher::>>("localization/measured_lines_in_field") .build() .await?; let _pose_hypotheses_pub = node - .publisher::>("localization/pose_hypotheses")? + .publisher::>("localization/pose_hypotheses") .build() .await?; let _updates_pub = node - .publisher::>>("localization/updates")? + .publisher::>>("localization/updates") .build() .await?; let _gyro_movement_pub = node - .publisher::("localization/gyro_movement")? + .publisher::("localization/gyro_movement") .build() .await?; let ground_to_field_pub = node - .publisher::>("ground_to_field")? + .publisher::>("ground_to_field") .build() .await?; let is_localization_converged_pub = node - .publisher::("is_localization_converged")? + .publisher::("is_localization_converged") .build() .await?; diff --git a/crates/nodes/look_around/src/lib.rs b/crates/nodes/look_around/src/lib.rs index 00a9ef38d2..006ff978b8 100644 --- a/crates/nodes/look_around/src/lib.rs +++ b/crates/nodes/look_around/src/lib.rs @@ -20,19 +20,19 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("look_around")?; let _motion_command_sub = node - .subscriber::("motion_command")? + .subscriber::("motion_command") .build() .await?; let _filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let _current_mode_pub = node - .publisher::("look_around_mode")? + .publisher::("look_around_mode") .build() .await?; let _look_around_target_joints_pub = node - .publisher::>("look_around_target_joints")? + .publisher::>("look_around_target_joints") .build() .await?; diff --git a/crates/nodes/look_at/src/lib.rs b/crates/nodes/look_at/src/lib.rs index f81772a635..9228fb4159 100644 --- a/crates/nodes/look_at/src/lib.rs +++ b/crates/nodes/look_at/src/lib.rs @@ -29,25 +29,22 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("look_at")?; let _camera_matrix_sub = node - .subscriber::("camera_matrix")? + .subscriber::("camera_matrix") .build() .await?; let _ground_to_robot_sub = node - .subscriber::>("ground_to_robot")? + .subscriber::>("ground_to_robot") .build() .await?; let _motion_command_sub = node - .subscriber::("motion_command")? + .subscriber::("motion_command") .build() .await?; let _serial_motor_states_sub = node - .subscriber::>("inputs/serial_motor_states")? - .build() - .await?; - let _look_at_pub = node - .publisher::>("look_at")? + .subscriber::>("inputs/serial_motor_states") .build() .await?; + let _look_at_pub = node.publisher::>("look_at").build().await?; pending::<()>().await; diff --git a/crates/nodes/low_command_publisher/src/lib.rs b/crates/nodes/low_command_publisher/src/lib.rs index c72847428a..52234e6cce 100644 --- a/crates/nodes/low_command_publisher/src/lib.rs +++ b/crates/nodes/low_command_publisher/src/lib.rs @@ -26,11 +26,11 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("command_sender")?; let collected_target_joint_positions_sub = node - .subscriber::>("collected_target_joint_positions")? + .subscriber::>("collected_target_joint_positions") .build() .await?; let low_command_pub = node - .publisher::("actions/low_command")? + .publisher::("actions/low_command") .build() .await?; diff --git a/crates/nodes/low_state_bridge/src/lib.rs b/crates/nodes/low_state_bridge/src/lib.rs index fd36c84bf2..fdce0aa655 100644 --- a/crates/nodes/low_state_bridge/src/lib.rs +++ b/crates/nodes/low_state_bridge/src/lib.rs @@ -22,19 +22,19 @@ async fn run(ctx: Arc) -> Result<()> { .map_err(|error| color_eyre::eyre::eyre!("{error}"))?; let low_state_pub = node - .publisher::("inputs/low_state")? + .publisher::("inputs/low_state") .build() .await?; let imu_state_pub = node - .publisher::("inputs/imu_state")? + .publisher::("inputs/imu_state") .build() .await?; let serial_motor_states_pub = node - .publisher::>("inputs/serial_motor_states")? + .publisher::>("inputs/serial_motor_states") .build() .await?; let parallel_motor_states_pub = node - .publisher::>>("inputs/parallel_motor_states")? + .publisher::>>("inputs/parallel_motor_states") .build() .await?; diff --git a/crates/nodes/message_filter/src/lib.rs b/crates/nodes/message_filter/src/lib.rs index 10e180d879..bf2c0eec16 100644 --- a/crates/nodes/message_filter/src/lib.rs +++ b/crates/nodes/message_filter/src/lib.rs @@ -15,7 +15,7 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("message_filter").build().await?; let player_number_sub = node - .subscriber::("player_number")? + .subscriber::("player_number") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -23,11 +23,11 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let message_sub = node - .subscriber::>("inputs/message")? + .subscriber::>("inputs/message") .build() .await?; let filtered_message_pub = node - .publisher::>("filtered_message")? + .publisher::>("filtered_message") .build() .await?; diff --git a/crates/nodes/message_handler/src/lib.rs b/crates/nodes/message_handler/src/lib.rs index 2f35d4b11d..6a8f864c1c 100644 --- a/crates/nodes/message_handler/src/lib.rs +++ b/crates/nodes/message_handler/src/lib.rs @@ -24,11 +24,11 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("message_receiver")?; let incoming_message_pub = node - .publisher::>("inputs/message")? + .publisher::>("inputs/message") .build() .await?; let outgoing_message_sub = node - .subscriber::("outputs/message")? + .subscriber::("outputs/message") .build() .await?; diff --git a/crates/nodes/microphone_recorder/src/lib.rs b/crates/nodes/microphone_recorder/src/lib.rs index 86e3a105a0..0f15493dcd 100644 --- a/crates/nodes/microphone_recorder/src/lib.rs +++ b/crates/nodes/microphone_recorder/src/lib.rs @@ -17,7 +17,7 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("microphone_recorder")?; let microphones_samples_pub = node - .publisher::("inputs/microphones_samples")? + .publisher::("inputs/microphones_samples") .build() .await?; diff --git a/crates/nodes/motor_commands_collector/src/lib.rs b/crates/nodes/motor_commands_collector/src/lib.rs index 1cd05a11bd..cfdb73199b 100644 --- a/crates/nodes/motor_commands_collector/src/lib.rs +++ b/crates/nodes/motor_commands_collector/src/lib.rs @@ -13,11 +13,11 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("motor_commands_collector").build().await?; let _head_target_joints_positions_sub = node - .subscriber::>("head_joints_command")? + .subscriber::>("head_joints_command") .build() .await?; let _collected_target_joint_positions_pub = node - .publisher::>("collected_target_joint_positions")? + .publisher::>("collected_target_joint_positions") .build() .await?; diff --git a/crates/nodes/obstacle_filter/src/lib.rs b/crates/nodes/obstacle_filter/src/lib.rs index 83cd0a237b..a8f74feca7 100644 --- a/crates/nodes/obstacle_filter/src/lib.rs +++ b/crates/nodes/obstacle_filter/src/lib.rs @@ -60,28 +60,31 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("obstacle_filter")?; let field_dimensions_cache = node - .create_cache::("field_dimensions", 1)? - .with_qos(QosProfile { + .subscriber::("field_dimensions") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let camera_matrix_cache = node - .create_cache::>("camera_matrix", 10)? + .subscriber::>("camera_matrix") + .cache(10) .with_stamp(|wrapper| wrapper.time) .build() .await?; let player_number_cache = node - .create_cache::("player_number", 1)? - .with_qos(QosProfile { + .subscriber::("player_number") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let player_states_subscriber = node - .subscriber::>>>("player_states")? + .subscriber::>>>("player_states") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -89,23 +92,27 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let current_odometry_to_last_odometry_cache = node - .create_cache::>("current_odometry_to_last_odometry", 10)? + .subscriber::>("current_odometry_to_last_odometry") + .cache(10) .build() .await?; let primary_state_cache = node - .create_cache::("primary_state", 1)? - .with_qos(QosProfile { + .subscriber::("primary_state") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let ground_to_field_cache = node - .create_cache::>("ground_to_field", 10)? + .subscriber::>("ground_to_field") + .cache(10) .build() .await?; let fall_down_state_cache = node - .create_cache::("inputs/fall_down_state", 10)? + .subscriber::("inputs/fall_down_state") + .cache(10) .build() .await?; @@ -124,13 +131,10 @@ async fn run(ctx: Arc) -> Result<()> { .build(); let obstacle_filter_hypotheses_pub = node - .publisher::>("obstacle_filter_hypotheses")? - .build() - .await?; - let obstacles_pub = node - .publisher::>("obstacles")? + .publisher::>("obstacle_filter_hypotheses") .build() .await?; + let obstacles_pub = node.publisher::>("obstacles").build().await?; let mut obstacle_filter = ObstacleFilter::default(); let mut last_processed_player_state_times = Players::new(None); diff --git a/crates/nodes/player_state_receiver/src/lib.rs b/crates/nodes/player_state_receiver/src/lib.rs index 39cf8efc69..6bb3654801 100644 --- a/crates/nodes/player_state_receiver/src/lib.rs +++ b/crates/nodes/player_state_receiver/src/lib.rs @@ -16,15 +16,15 @@ pub fn run_boxed(ctx: Arc) -> Pin> + pub async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("player_state_receiver").build().await?; let filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let filtered_message_sub = node - .subscriber::>("filtered_message")? + .subscriber::>("filtered_message") .build() .await?; let player_states_pub = node - .publisher::>>>("player_states")? + .publisher::>>>("player_states") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() diff --git a/crates/nodes/primary_state_filter/src/lib.rs b/crates/nodes/primary_state_filter/src/lib.rs index b30e9a2a16..598564eb4e 100644 --- a/crates/nodes/primary_state_filter/src/lib.rs +++ b/crates/nodes/primary_state_filter/src/lib.rs @@ -29,29 +29,31 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("primary_state_filter")?; let player_number_cache = node - .create_cache::("player_number", 1)? - .with_qos(QosProfile { + .subscriber::("player_number") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let buttons_sub = node - .subscriber::>>("buttons")? + .subscriber::>>("buttons") .build() .await?; let is_safe_pose_cache = node - .create_cache::("is_safe_pose", 1)? + .subscriber::("is_safe_pose") + .cache(1) .build() .await?; let primary_state_pub = node - .publisher::("primary_state")? + .publisher::("primary_state") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() diff --git a/crates/nodes/robot_mode_handler/src/lib.rs b/crates/nodes/robot_mode_handler/src/lib.rs index 59132d95f8..33fb06723c 100644 --- a/crates/nodes/robot_mode_handler/src/lib.rs +++ b/crates/nodes/robot_mode_handler/src/lib.rs @@ -26,7 +26,7 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("robot_mode_handler")?; let primary_state_sub = node - .subscriber::("primary_state")? + .subscriber::("primary_state") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -34,15 +34,15 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let buttons_sub = node - .subscriber::>>("buttons")? + .subscriber::>>("buttons") .build() .await?; let high_level_command_pub = node - .publisher::("commands/high_level_command")? + .publisher::("commands/high_level_command") .build() .await?; let get_robot_mode_client = node - .create_service_client::("services/get_robot_mode")? + .service_client::("services/get_robot_mode") .build() .await?; diff --git a/crates/nodes/rotate_head/src/lib.rs b/crates/nodes/rotate_head/src/lib.rs index a2c8d8b689..c721f807ac 100644 --- a/crates/nodes/rotate_head/src/lib.rs +++ b/crates/nodes/rotate_head/src/lib.rs @@ -23,11 +23,11 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("rotate_head")?; let _get_robot_mode_client = node - .create_service_client::("services/get_robot_mode")? + .service_client::("services/get_robot_mode") .build() .await?; let _head_joints_sub = node - .subscriber::>("head_joints_command")? + .subscriber::>("head_joints_command") .build() .await?; diff --git a/crates/nodes/rule_obstacle_composer/src/lib.rs b/crates/nodes/rule_obstacle_composer/src/lib.rs index f78d99989e..e71a05f9ba 100644 --- a/crates/nodes/rule_obstacle_composer/src/lib.rs +++ b/crates/nodes/rule_obstacle_composer/src/lib.rs @@ -30,23 +30,25 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("rule_obstacle_composer")?; let field_dimensions_cache = node - .create_cache::("field_dimensions", 1)? - .with_qos(QosProfile { + .subscriber::("field_dimensions") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let ball_state_cache = node - .create_cache::>("ball_state", 1)? + .subscriber::>("ball_state") + .cache(1) .build() .await?; let rule_obstacles_pub = node - .publisher::>("rule_obstacles")? + .publisher::>("rule_obstacles") .build() .await?; diff --git a/crates/nodes/safe_pose_checker/src/lib.rs b/crates/nodes/safe_pose_checker/src/lib.rs index bc8338c4cc..fc35816ed8 100644 --- a/crates/nodes/safe_pose_checker/src/lib.rs +++ b/crates/nodes/safe_pose_checker/src/lib.rs @@ -31,30 +31,31 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("safe_pose_checker")?; let imu_state_sub = node - .subscriber::("inputs/imu_state")? + .subscriber::("inputs/imu_state") .build() .await?; let serial_motor_states_cache = node - .create_cache::>("inputs/serial_motor_states", 10)? + .subscriber::>("inputs/serial_motor_states") + .cache(10) .build() .await?; let joint_position_difference_to_safe_pub = node - .publisher::("joint_position_difference_to_safe")? + .publisher::("joint_position_difference_to_safe") .build() .await?; let joint_velocities_difference_to_safe_pub = node - .publisher::("joint_velocities_difference_to_safe")? + .publisher::("joint_velocities_difference_to_safe") .build() .await?; let angular_velocities_difference_to_safe_pub = node - .publisher::>("angular_velocities_difference_to_safe")? + .publisher::>("angular_velocities_difference_to_safe") .build() .await?; let linear_accelerations_difference_to_safe_pub = node - .publisher::>("linear_accelerations_difference_to_safe")? + .publisher::>("linear_accelerations_difference_to_safe") .build() .await?; - let is_safe_pose_pub = node.publisher::("is_safe_pose")?.build().await?; + let is_safe_pose_pub = node.publisher::("is_safe_pose").build().await?; loop { let parameters_snapshot = parameters.snapshot(); let parameters = parameters_snapshot.typed(); diff --git a/crates/nodes/search_suggestor/src/lib.rs b/crates/nodes/search_suggestor/src/lib.rs index 14441445a8..5a664d188a 100644 --- a/crates/nodes/search_suggestor/src/lib.rs +++ b/crates/nodes/search_suggestor/src/lib.rs @@ -29,7 +29,7 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("search_suggestor")?; let field_dimensions_sub = node - .subscriber::("field_dimensions")? + .subscriber::("field_dimensions") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -37,41 +37,43 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let ball_position_sub = node - .subscriber::>>("ball_filter/ball_position")? + .subscriber::>>("ball_filter/ball_position") .build() .await?; let hypothetical_ball_positions_sub = node .subscriber::>>( "ball_filter/hypothetical_ball_positions", - )? + ) .build() .await?; let ground_to_field_cache = node - .create_cache::>("ground_to_field", 10)? + .subscriber::>("ground_to_field") + .cache(10) .build() .await?; let primary_state_cache = node - .create_cache::("primary_state", 1)? - .with_qos(QosProfile { + .subscriber::("primary_state") + .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() }) + .cache(1) .build() .await?; let filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let network_message_sub = node - .subscriber::>("filtered_message")? + .subscriber::>("filtered_message") .build() .await?; let additional_heatmap_pub = node - .publisher::("ball_search_heatmap")? + .publisher::("ball_search_heatmap") .build() .await?; let suggested_search_position_pub = node - .publisher::>("suggested_search_position")? + .publisher::>("suggested_search_position") .build() .await?; diff --git a/crates/nodes/segment_filter/src/lib.rs b/crates/nodes/segment_filter/src/lib.rs index f7dd134333..6d5ce18629 100644 --- a/crates/nodes/segment_filter/src/lib.rs +++ b/crates/nodes/segment_filter/src/lib.rs @@ -20,16 +20,17 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("segment_filter").build().await?; let field_border_sub = node - .subscriber::>>("field_border")? + .subscriber::>>("field_border") .build() .await?; let image_segments_cache = node - .create_cache::>("image_segments", 10)? + .subscriber::>("image_segments") + .cache(10) .with_stamp(|w: &TimeWrapper| w.time) .build() .await?; let filtered_segments_pub = node - .publisher::>("filtered_segments")? + .publisher::>("filtered_segments") .build() .await?; diff --git a/crates/nodes/stand_up/src/lib.rs b/crates/nodes/stand_up/src/lib.rs index 59f949319e..437e3a75a6 100644 --- a/crates/nodes/stand_up/src/lib.rs +++ b/crates/nodes/stand_up/src/lib.rs @@ -15,11 +15,11 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("stand_up").build().await?; let _get_robot_mode_client = node - .create_service_client::("services/get_robot_mode")? + .service_client::("services/get_robot_mode") .build() .await?; let _motion_command_sub = node - .subscriber::("motion_command")? + .subscriber::("motion_command") .build() .await?; diff --git a/crates/nodes/support_foot_estimator/src/lib.rs b/crates/nodes/support_foot_estimator/src/lib.rs index c610011d7b..27f07826e7 100644 --- a/crates/nodes/support_foot_estimator/src/lib.rs +++ b/crates/nodes/support_foot_estimator/src/lib.rs @@ -29,21 +29,23 @@ async fn run(ctx: Arc) -> Result<()> { let parameters = node.bind_parameter_as::("support_foot_estimator")?; let imu_state_sub = node - .subscriber::("inputs/imu_state")? + .subscriber::("inputs/imu_state") .build() .await?; let robot_kinematics_cache = node - .create_cache::>("robot_kinematics", 10)? + .subscriber::>("robot_kinematics") + .cache(10) .with_stamp(|wrapper| wrapper.time) .build() .await?; let fall_down_state_cache = node - .create_cache::("inputs/fall_down_state", 10)? + .subscriber::("inputs/fall_down_state") + .cache(10) .build() .await?; let support_foot_pub = node - .publisher::>>("support_foot")? + .publisher::>>("support_foot") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() diff --git a/crates/nodes/team_ball_receiver/src/lib.rs b/crates/nodes/team_ball_receiver/src/lib.rs index 1f7a4af6ff..b6690df6d9 100644 --- a/crates/nodes/team_ball_receiver/src/lib.rs +++ b/crates/nodes/team_ball_receiver/src/lib.rs @@ -26,19 +26,19 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("team_ball_receiver")?; let _filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let _network_message_sub = node - .subscriber::("filtered_message")? + .subscriber::("filtered_message") .build() .await?; let _team_balls_pub = node - .publisher::>>>("team_balls")? + .publisher::>>>("team_balls") .build() .await?; let _team_ball_pub = node - .publisher::>("team_ball")? + .publisher::>("team_ball") .build() .await?; diff --git a/crates/nodes/time_to_reach_kick_position/src/lib.rs b/crates/nodes/time_to_reach_kick_position/src/lib.rs index 30765440ad..988e0e37b3 100644 --- a/crates/nodes/time_to_reach_kick_position/src/lib.rs +++ b/crates/nodes/time_to_reach_kick_position/src/lib.rs @@ -15,9 +15,9 @@ async fn run(ctx: Arc) -> Result<()> { .create_node("time_to_reach_kick_position") .build() .await?; - let _ball_state_sub = node.subscriber::("ball_state")?.build().await?; + let _ball_state_sub = node.subscriber::("ball_state").build().await?; let _time_to_reach_kick_position_pub = node - .publisher::("time_to_reach_kick_position")? + .publisher::("time_to_reach_kick_position") .build() .await?; diff --git a/crates/nodes/walking/src/lib.rs b/crates/nodes/walking/src/lib.rs index ea48b0f36b..bce3327ffb 100644 --- a/crates/nodes/walking/src/lib.rs +++ b/crates/nodes/walking/src/lib.rs @@ -25,15 +25,15 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("walking")?; let _get_robot_mode_client = node - .create_service_client::("services/get_robot_mode")? + .service_client::("services/get_robot_mode") .build() .await?; let _motion_command_sub = node - .subscriber::("motion_command")? + .subscriber::("motion_command") .build() .await?; let _step_pub = node - .publisher::("additional_outputs/walking_step")? + .publisher::("additional_outputs/walking_step") .build() .await?; diff --git a/crates/nodes/whistle_detection/src/lib.rs b/crates/nodes/whistle_detection/src/lib.rs index 7a10647ddd..db0b4ecd0a 100644 --- a/crates/nodes/whistle_detection/src/lib.rs +++ b/crates/nodes/whistle_detection/src/lib.rs @@ -18,18 +18,18 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("whistle_detection").build().await?; let _parameters = node.bind_parameter_as::("whistle_detection")?; - let _samples_sub = node.subscriber::("samples")?.build().await?; + let _samples_sub = node.subscriber::("samples").build().await?; // TODO: restructure type layout here, do not use blank tuples // let _audio_spectrums_pub = node // .publisher::>>("audio_spectrums") // .build() // .await?; let _detection_infos_pub = node - .publisher::>("detection_infos")? + .publisher::>("detection_infos") .build() .await?; let _detected_whistle_pub = node - .publisher::("detected_whistle")? + .publisher::("detected_whistle") .build() .await?; diff --git a/crates/nodes/whistle_filter/src/lib.rs b/crates/nodes/whistle_filter/src/lib.rs index ef5e3efd73..39ab445766 100644 --- a/crates/nodes/whistle_filter/src/lib.rs +++ b/crates/nodes/whistle_filter/src/lib.rs @@ -23,11 +23,11 @@ async fn run(ctx: Arc) -> Result<()> { let _parameters = node.bind_parameter_as::("whistle_filter")?; let _detected_whistle_sub = node - .subscriber::("detected_whistle")? + .subscriber::("detected_whistle") .build() .await?; let _filtered_whistle_pub = node - .publisher::("filtered_whistle")? + .publisher::("filtered_whistle") .build() .await?; diff --git a/crates/nodes/world_state_composer/src/lib.rs b/crates/nodes/world_state_composer/src/lib.rs index 8a81eca92d..85275fc571 100644 --- a/crates/nodes/world_state_composer/src/lib.rs +++ b/crates/nodes/world_state_composer/src/lib.rs @@ -25,7 +25,7 @@ async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("world_state_composer").build().await?; let _player_number_sub = node - .subscriber::("player_number")? + .subscriber::("player_number") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -33,32 +33,32 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let _fall_down_state_sub = node - .subscriber::("inputs/fall_down_state")? + .subscriber::("inputs/fall_down_state") .build() .await?; - let _ball_sub = node.subscriber::("ball_state")?.build().await?; + let _ball_sub = node.subscriber::("ball_state").build().await?; let _filtered_game_controller_state_sub = node - .subscriber::("filtered_game_controller_state")? + .subscriber::("filtered_game_controller_state") .build() .await?; let _ground_to_field_sub = node - .subscriber::>("ground_to_field")? + .subscriber::>("ground_to_field") .build() .await?; let _hypothetical_ball_position_sub = node - .subscriber::>>("hypothetical_ball_positions")? + .subscriber::>>("hypothetical_ball_positions") .build() .await?; let _obstacles_sub = node - .subscriber::>("obstacles")? + .subscriber::>("obstacles") .build() .await?; let _position_of_interest_sub = node - .subscriber::>("position_of_interest")? + .subscriber::>("position_of_interest") .build() .await?; let _primary_state_sub = node - .subscriber::("primary_state")? + .subscriber::("primary_state") .qos(QosProfile { durability: QosDurability::TransientLocal, ..Default::default() @@ -66,18 +66,18 @@ async fn run(ctx: Arc) -> Result<()> { .build() .await?; let _rule_ball_sub = node - .subscriber::("rule_ball_state")? + .subscriber::("rule_ball_state") .build() .await?; let _rule_obstacles_sub = node - .subscriber::>("rule_obstacles")? + .subscriber::>("rule_obstacles") .build() .await?; let _suggested_search_position_sub = node - .subscriber::>("suggested_search_position")? + .subscriber::>("suggested_search_position") .build() .await?; - let _world_state_pub = node.publisher::("world_state")?.build().await?; + let _world_state_pub = node.publisher::("world_state").build().await?; pending::<()>().await; diff --git a/crates/nodes/world_to_field_provider/src/lib.rs b/crates/nodes/world_to_field_provider/src/lib.rs index bdcfd274a2..33e2a2b1cc 100644 --- a/crates/nodes/world_to_field_provider/src/lib.rs +++ b/crates/nodes/world_to_field_provider/src/lib.rs @@ -15,12 +15,12 @@ pub fn run_boxed(ctx: Arc) -> Pin> + async fn run(ctx: Arc) -> Result<()> { let node = ctx.create_node("world_to_field_provider").build().await?; let game_controller_state_sub = node - .subscriber::>("game_controller_state")? + .subscriber::>("game_controller_state") .build() .await?; let world_to_field_pub = node - .publisher::>>("world_to_field")? + .publisher::>>("world_to_field") .build() .await?; diff --git a/crates/ros-z-cli/src/app/context.rs b/crates/ros-z-cli/src/app/context.rs index d566a6c509..15db818017 100644 --- a/crates/ros-z-cli/src/app/context.rs +++ b/crates/ros-z-cli/src/app/context.rs @@ -6,7 +6,7 @@ use std::{ use color_eyre::eyre::{Result, WrapErr}; use ros_z::{ context::{Context, ContextBuilder}, - dynamic::DynamicSubscriberBuilder, + dynamic::DynamicSubscriber, graph::GraphSnapshot, node::Node, parameter::RemoteParameterClient, @@ -83,16 +83,16 @@ impl AppContext { } } - pub async fn create_dynamic_subscriber_builder( + pub async fn create_dynamic_subscriber( &self, topic: &str, discovery_timeout: Duration, - ) -> Result { - let builder = self - .node + ) -> Result { + self.node .dynamic_subscriber_auto(topic, discovery_timeout) - .await?; - Ok(builder) + .build() + .await + .wrap_err_with(|| format!("failed to subscribe to {topic}")) } pub fn parameter_client(&self, target_fqn: &str) -> Result { diff --git a/crates/ros-z-cli/src/commands/echo.rs b/crates/ros-z-cli/src/commands/echo.rs index 1b8ddc47f8..493db622ee 100644 --- a/crates/ros-z-cli/src/commands/echo.rs +++ b/crates/ros-z-cli/src/commands/echo.rs @@ -27,14 +27,9 @@ pub async fn run( count: Option, timeout: Option, ) -> Result<()> { - let subscriber_builder = app - .create_dynamic_subscriber_builder(topic, TYPE_DISCOVERY_TIMEOUT) - .await - .wrap_err_with(|| format!("failed to subscribe to {topic}"))?; - let subscriber = subscriber_builder - .build() - .await - .wrap_err_with(|| format!("failed to subscribe to {topic}"))?; + let subscriber = app + .create_dynamic_subscriber(topic, TYPE_DISCOVERY_TIMEOUT) + .await?; let _schema = subscriber .schema() .ok_or_else(|| eyre!("dynamic subscriber missing schema for {topic}"))?; diff --git a/crates/ros-z-cli/src/commands/schema.rs b/crates/ros-z-cli/src/commands/schema.rs index 796d0ca147..31b2f253ba 100644 --- a/crates/ros-z-cli/src/commands/schema.rs +++ b/crates/ros-z-cli/src/commands/schema.rs @@ -27,7 +27,7 @@ pub async fn run( let service_name = format!("{node}/get_schema"); let client = app .node() - .create_service_client::(&service_name)? + .service_client::(&service_name) .build() .await?; let response = client diff --git a/crates/ros-z-cli/tests/e2e.rs b/crates/ros-z-cli/tests/e2e.rs index 627f96687c..b9f2bba082 100644 --- a/crates/ros-z-cli/tests/e2e.rs +++ b/crates/ros-z-cli/tests/e2e.rs @@ -371,11 +371,8 @@ impl GraphFixture { .await?; let topic = "/cli_e2e/telemetry".to_string(); let service = "/cli_e2e/add_two_ints".to_string(); - let publisher = node.publisher::(&topic)?.build().await?; - let service_server = node - .create_service_server::(&service)? - .build() - .await?; + let publisher = node.publisher::(&topic).build().await?; + let service_server = node.service_server::(&service).build().await?; Ok(Self { topic, @@ -406,7 +403,7 @@ impl PublishingFixture { .with_namespace("/cli_e2e") .build() .await?; - let publisher = node.publisher::(topic)?.build().await?; + let publisher = node.publisher::(topic).build().await?; Ok(Self { topic: topic.to_string(), @@ -683,7 +680,7 @@ async fn schema_resolves_string_publisher_schema() -> TestResult { .build() .await?; let _publisher = node - .publisher::("/cli_e2e/schema_string")? + .publisher::("/cli_e2e/schema_string") .build() .await?; let node_fqn = "/cli_e2e/string_schema_fixture"; diff --git a/crates/ros-z-debug/src/manager.rs b/crates/ros-z-debug/src/manager.rs index 2ed6682aa2..5a5114e9c3 100644 --- a/crates/ros-z-debug/src/manager.rs +++ b/crates/ros-z-debug/src/manager.rs @@ -207,7 +207,7 @@ impl TypedSubscriptionBuilder<'_, T> { let subscriber = self .manager .node() - .subscriber::(&resolved_topic)? + .subscriber::(&resolved_topic) .build() .await?; let type_info = subscriber.entity().type_info.clone(); @@ -295,7 +295,6 @@ impl DynamicSubscriptionBuilder<'_> { &resolved_topic, self.manager.options.schema_discovery_timeout(), ) - .await? .build() .await?; let type_info = subscriber.entity().type_info.clone(); diff --git a/crates/ros-z-debug/tests/integration.rs b/crates/ros-z-debug/tests/integration.rs index 33a1acc2bc..6b7ea195c3 100644 --- a/crates/ros-z-debug/tests/integration.rs +++ b/crates/ros-z-debug/tests/integration.rs @@ -56,7 +56,6 @@ async fn typed_subscription_receives_latest_sample() { ); let publisher = publisher_node .publisher::("debug_text") - .expect("publisher builder") .build() .await .expect("publisher"); @@ -132,7 +131,6 @@ async fn typed_subscription_resolves_relative_topic_against_target_namespace() { ); let publisher = publisher_node .publisher::("/alpha/debug_text") - .expect("publisher builder") .build() .await .expect("publisher"); @@ -195,7 +193,6 @@ async fn dynamic_subscription_renders_json_view() { ); let publisher = publisher_node .dynamic_publisher("debug_dynamic", type_info, schema.clone()) - .expect("dynamic publisher builder") .build() .await .expect("dynamic publisher"); diff --git a/crates/ros-z-streams/src/announce.rs b/crates/ros-z-streams/src/announce.rs index 925f784266..71ba9eb64b 100644 --- a/crates/ros-z-streams/src/announce.rs +++ b/crates/ros-z-streams/src/announce.rs @@ -73,11 +73,8 @@ impl CreateAnnouncingPublisher for Node { &self, topic: &str, ) -> Result> { - let data_publisher = self.publisher(topic)?.build().await?; - let announcement_publisher = self - .publisher(&format!("{topic}/announce"))? - .build() - .await?; + let data_publisher = self.publisher(topic).build().await?; + let announcement_publisher = self.publisher(&format!("{topic}/announce")).build().await?; Ok(AnnouncingPublisher { data_publisher, diff --git a/crates/ros-z-streams/src/future_queue.rs b/crates/ros-z-streams/src/future_queue.rs index 4441fa52db..3bc05349aa 100644 --- a/crates/ros-z-streams/src/future_queue.rs +++ b/crates/ros-z-streams/src/future_queue.rs @@ -149,7 +149,7 @@ impl CreateFutureQueue for Node { transit_lag: Duration, ) -> Result> { let data_subscriber = self - .subscriber(topic)? + .subscriber(topic) .qos(QosProfile { history: QosHistory::KeepAll, ..Default::default() @@ -157,7 +157,7 @@ impl CreateFutureQueue for Node { .build() .await?; let announcement_subscriber = self - .subscriber(&format!("{}/announce", topic))? + .subscriber(&format!("{}/announce", topic)) .qos(QosProfile { history: QosHistory::KeepAll, ..Default::default() diff --git a/crates/ros-z/README.md b/crates/ros-z/README.md index ab856bd709..0354f0d559 100644 --- a/crates/ros-z/README.md +++ b/crates/ros-z/README.md @@ -27,19 +27,31 @@ C/C++ runtime dependencies. ```rust use ros_z::prelude::*; -# async fn demo() -> ros_z::Result<()> { -let context = ContextBuilder::default().build().await?; -let node = context.create_node("talker").build().await?; -let publisher = node.publisher::("/chatter")?.build().await?; - -publisher.publish(&"hello".to_owned()).await?; -# Ok(()) -# } +async fn demo() -> ros_z::Result<()> { + let context = ContextBuilder::default().build().await?; + let node = context.create_node("talker").build().await?; + let publisher = node.publisher::("/chatter").build().await?; + + publisher.publish(&"hello".to_owned()).await?; + Ok(()) +} ``` Builders that create runtime resources are async. Build contexts, nodes, publishers, subscribers, services, and caches inside a Tokio-compatible runtime. -Endpoint factory methods validate schema/type metadata immediately and return `Result`; `.build().await` only creates runtime Zenoh resources. +Endpoint factories return builders directly and defer schema, type, and graph-name +validation until `.build().await`. + +Core endpoint builders use one `?` at build time. Service examples assume a +user-defined `AddTwoInts` type that implements `Service` and `ServiceTypeInfo`: + +```rust,ignore +let publisher = node.publisher::("/chatter").build().await?; +let subscriber = node.subscriber::("/chatter").build().await?; +let cache = node.subscriber::("/chatter").cache(200).build().await?; +let server = node.service_server::("add_two_ints").build().await?; +let client = node.service_client::("add_two_ints").build().await?; +``` ## Name Rules diff --git a/crates/ros-z/examples/custom_message/navigation_client.rs b/crates/ros-z/examples/custom_message/navigation_client.rs index ca2078d5ed..b73e856831 100644 --- a/crates/ros-z/examples/custom_message/navigation_client.rs +++ b/crates/ros-z/examples/custom_message/navigation_client.rs @@ -8,7 +8,7 @@ async fn main() -> Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("navigation_client").build().await?; let service_client = node - .create_service_client::("navigate_to")? + .service_client::("navigate_to") .build() .await?; diff --git a/crates/ros-z/examples/custom_message/navigation_server.rs b/crates/ros-z/examples/custom_message/navigation_server.rs index 1b865c6900..c948755da0 100644 --- a/crates/ros-z/examples/custom_message/navigation_server.rs +++ b/crates/ros-z/examples/custom_message/navigation_server.rs @@ -8,7 +8,7 @@ async fn main() -> Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("navigation_server").build().await?; let mut service_server = node - .create_service_server::("navigate_to")? + .service_server::("navigate_to") .build() .await?; diff --git a/crates/ros-z/examples/custom_message/status_publisher.rs b/crates/ros-z/examples/custom_message/status_publisher.rs index de0a2c3f4b..1fca4545d4 100644 --- a/crates/ros-z/examples/custom_message/status_publisher.rs +++ b/crates/ros-z/examples/custom_message/status_publisher.rs @@ -13,7 +13,7 @@ async fn main() -> Result<()> { .build() .await?; let publisher = node - .publisher::("robot_status")? + .publisher::("robot_status") .build() .await?; diff --git a/crates/ros-z/examples/custom_message/status_subscriber.rs b/crates/ros-z/examples/custom_message/status_subscriber.rs index a44cb45f50..c10f997042 100644 --- a/crates/ros-z/examples/custom_message/status_subscriber.rs +++ b/crates/ros-z/examples/custom_message/status_subscriber.rs @@ -11,7 +11,7 @@ async fn main() -> Result<()> { .build() .await?; let subscriber = node - .subscriber::("robot_status")? + .subscriber::("robot_status") .build() .await?; diff --git a/crates/ros-z/examples/pubsub/listener.rs b/crates/ros-z/examples/pubsub/listener.rs index 13b769b1b6..962dff7ff3 100644 --- a/crates/ros-z/examples/pubsub/listener.rs +++ b/crates/ros-z/examples/pubsub/listener.rs @@ -6,7 +6,7 @@ async fn main() -> Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("listener").build().await?; - let subscriber = node.subscriber::("chatter")?.build().await?; + let subscriber = node.subscriber::("chatter").build().await?; println!("Listening for String messages on /chatter..."); while let Ok(received) = subscriber.recv_with_metadata().await { diff --git a/crates/ros-z/examples/pubsub/talker.rs b/crates/ros-z/examples/pubsub/talker.rs index 193fa5a673..6f48a9d5cf 100644 --- a/crates/ros-z/examples/pubsub/talker.rs +++ b/crates/ros-z/examples/pubsub/talker.rs @@ -8,7 +8,7 @@ async fn main() -> Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("talker").build().await?; - let publisher = node.publisher::("chatter")?.build().await?; + let publisher = node.publisher::("chatter").build().await?; let mut count = 1_u64; loop { diff --git a/crates/ros-z/examples/service/client.rs b/crates/ros-z/examples/service/client.rs index 1f42ecd6c3..a06108faf8 100644 --- a/crates/ros-z/examples/service/client.rs +++ b/crates/ros-z/examples/service/client.rs @@ -10,7 +10,7 @@ async fn main() -> Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("add_two_ints_client").build().await?; let service_client = node - .create_service_client::("add_two_ints")? + .service_client::("add_two_ints") .build() .await?; diff --git a/crates/ros-z/examples/service/server.rs b/crates/ros-z/examples/service/server.rs index 5756816fd1..cc25b0d370 100644 --- a/crates/ros-z/examples/service/server.rs +++ b/crates/ros-z/examples/service/server.rs @@ -8,7 +8,7 @@ async fn main() -> Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("add_two_ints_server").build().await?; let mut service_server = node - .create_service_server::("add_two_ints")? + .service_server::("add_two_ints") .build() .await?; diff --git a/crates/ros-z/src/cache.rs b/crates/ros-z/src/cache.rs index 572adaf984..8d741da4c2 100644 --- a/crates/ros-z/src/cache.rs +++ b/crates/ros-z/src/cache.rs @@ -27,7 +27,7 @@ //! let node = context.create_node("cache_demo").build().await?; //! //! // Zero-config: indexed by Zenoh transport timestamp -//! let cache = node.create_cache::("/chatter", 200)?.build().await?; +//! let cache = node.subscriber::("/chatter").cache(200).build().await?; //! //! let now = Time::from_wallclock(std::time::SystemTime::now()); //! let window = cache.get_interval(now - Duration::from_millis(100), now); @@ -44,7 +44,6 @@ use tracing::{debug, warn}; use crate::Result; use crate::message::{SerdeCdrCodec, WireDecoder}; use crate::pubsub::SubscriberBuilder; -use crate::qos::QosProfile; use crate::time::Time; // --------------------------------------------------------------------------- @@ -210,7 +209,7 @@ impl CacheInner { /// messages. /// /// Built via [`CacheBuilder`], created through -/// [`Node::create_cache`](crate::node::Node::create_cache). +/// `node.subscriber::(topic).cache(capacity)`. /// /// Messages are stored as [`Arc`] so query methods return shared references /// without deep-copying the message payload. @@ -355,7 +354,7 @@ impl Cache { /// Builder for [`Cache`]. /// -/// Created by [`Node::create_cache`](crate::node::Node::create_cache). +/// Created by `node.subscriber::(topic).cache(capacity)`. /// Use [`with_stamp`](CacheBuilder::with_stamp) to switch from the default /// Zenoh transport timestamp to an application-level extractor. pub struct CacheBuilder, Stamp = ZenohStamp> { @@ -364,6 +363,37 @@ pub struct CacheBuilder, Stamp = ZenohStamp> { stamp: Stamp, } +impl SubscriberBuilder +where + S: for<'a> WireDecoder = &'a [u8], Output = T>, +{ + /// Build a timestamp-indexed cache from this subscriber builder. + /// + /// `capacity` is the maximum number of messages retained by the cache. A + /// capacity of `0` keeps the subscriber alive but stores no messages. By + /// default, samples are indexed by their Zenoh transport timestamp; call + /// [`CacheBuilder::with_stamp`] to use an application-level timestamp such + /// as `header.stamp` instead. + /// + /// Configure subscriber options such as QoS, locality, or transient-local + /// replay before calling `cache`, because this method switches from the + /// subscriber builder to a cache builder. + /// + /// # Example + /// + /// ```rust,ignore + /// let cache = node + /// .subscriber::("/chatter") + /// .qos(qos) + /// .cache(200) + /// .build() + /// .await?; + /// ``` + pub fn cache(self, capacity: usize) -> CacheBuilder { + CacheBuilder::new(self, capacity) + } +} + impl CacheBuilder { pub(crate) fn new(sub_builder: SubscriberBuilder, capacity: usize) -> Self { Self { @@ -396,15 +426,6 @@ impl CacheBuilder { self.capacity = capacity; self } - - /// Apply a QoS profile to the underlying subscriber. - pub fn with_qos(mut self, qos: QosProfile) -> Self - where - T: Send + Sync + 'static, - { - self.sub_builder = self.sub_builder.qos(qos); - self - } } impl CacheBuilder @@ -481,15 +502,6 @@ where self.capacity = capacity; self } - - /// Apply a QoS profile to the underlying subscriber. - pub fn with_qos(mut self, qos: QosProfile) -> Self - where - T: Send + Sync + 'static, - { - self.sub_builder = self.sub_builder.qos(qos); - self - } } impl CacheBuilder> diff --git a/crates/ros-z/src/dynamic/discovery.rs b/crates/ros-z/src/dynamic/discovery.rs index 50abb2cab6..a20c28d675 100644 --- a/crates/ros-z/src/dynamic/discovery.rs +++ b/crates/ros-z/src/dynamic/discovery.rs @@ -4,12 +4,15 @@ use std::time::Duration; use itertools::Itertools; use crate::{ - dynamic::{DynamicError, Schema}, + dynamic::{DynamicCdrCodec, DynamicError, DynamicPayload, DynamicSubscriber, Schema}, + endpoint_builder::{EndpointBuilderContext, MessageEndpointType}, entity::{EndpointEntity, SchemaHash, TypeInfo}, graph::Graph, - node::Node, + pubsub::{RawSubscriber, SubscriberBuilder, SubscriberOptions}, + qos::QosProfile, topic_name::qualify_topic_name, }; +use tracing::info; #[derive(Debug, Clone)] pub struct DiscoveredTopicSchema { @@ -93,26 +96,33 @@ fn collect_topic_schema_candidates( collect_topic_schema_candidates_from_publishers(&publishers, qualified_topic) } -pub(crate) struct SchemaDiscovery<'a> { - node: &'a Node, +pub(crate) struct SchemaDiscovery { + context: EndpointBuilderContext, timeout: Duration, } -impl<'a> SchemaDiscovery<'a> { - pub(crate) fn new(node: &'a Node, timeout: Duration) -> Self { - Self { node, timeout } +impl SchemaDiscovery { + pub(crate) fn new(context: EndpointBuilderContext, timeout: Duration) -> Self { + Self { context, timeout } } pub(crate) async fn discover( &self, topic: &str, ) -> Result { - let qualified_topic = qualify_topic_name(topic, self.node.namespace(), self.node.name()) - .map_err(|error| { - DynamicError::SchemaNotFound(format!("Failed to qualify topic: {error}")) - })?; + let qualified_topic = + qualify_topic_name(topic, &self.context.node.namespace, &self.context.node.name) + .map_err(|error| DynamicError::name("discovering topic schema", error))?; + + self.discover_qualified(qualified_topic).await + } + + async fn discover_qualified( + &self, + qualified_topic: String, + ) -> Result { let candidates = - collect_topic_schema_candidates(self.node.graph().as_ref(), &qualified_topic)?; + collect_topic_schema_candidates(self.context.graph.as_ref(), &qualified_topic)?; let (root_name, schema, schema_hash) = self.try_schema_service(&candidates[..]).await?; @@ -131,7 +141,7 @@ impl<'a> SchemaDiscovery<'a> { let mut last_error = None; for candidate in candidates { - match super::schema_query::query_schema(self.node, candidate, self.timeout).await { + match super::schema_query::query_schema(&self.context, candidate, self.timeout).await { Ok(result) => return Ok(result), Err(error) => last_error = Some(error), } @@ -143,6 +153,172 @@ impl<'a> SchemaDiscovery<'a> { } } +/// Builder for dynamic subscribers that discover their schema at build time. +/// +/// Create this with [`crate::node::Node::dynamic_subscriber_auto`]. Schema +/// discovery runs in [`build`](Self::build), so construction and option +/// configuration remain infallible. This builder exposes subscriber options +/// that do not require knowing the message schema up front. Use [`raw`](Self::raw) +/// for raw sample delivery after discovery. +#[derive(Debug)] +pub struct DynamicSubscriberDiscoveryBuilder { + context: EndpointBuilderContext, + topic: String, + discovery_timeout: Duration, + options: SubscriberOptions, +} + +/// Builder for raw dynamic subscribers that discover schema metadata at build time. +#[derive(Debug)] +pub struct DynamicRawSubscriberDiscoveryBuilder { + context: EndpointBuilderContext, + topic: String, + discovery_timeout: Duration, + options: SubscriberOptions, +} + +async fn discover_dynamic_subscriber_builder( + context: EndpointBuilderContext, + topic: String, + discovery_timeout: Duration, + options: SubscriberOptions, +) -> crate::Result> { + let qualified_topic = qualify_topic_name(&topic, &context.node.namespace, &context.node.name) + .map_err(|source| crate::Error::topic_name(topic.clone(), source))?; + + let discovered = SchemaDiscovery::new(context.clone(), discovery_timeout) + .discover_qualified(qualified_topic) + .await?; + + info!( + "[NOD] Discovered schema for topic {}: {} (hash: {})", + discovered.qualified_topic, + discovered.root_name, + discovered.schema_hash.to_hash_string() + ); + + Ok(SubscriberBuilder::::new( + context, + topic, + MessageEndpointType::prevalidated_dynamic(discovered.type_info(), discovered.schema), + ) + .options(options)) +} + +impl DynamicSubscriberDiscoveryBuilder { + pub(crate) fn new( + context: EndpointBuilderContext, + topic: String, + discovery_timeout: Duration, + ) -> Self { + Self { + context, + topic, + discovery_timeout, + options: SubscriberOptions::default(), + } + } + + /// Set the QoS profile used by the built dynamic subscriber. + /// + /// This does not affect the schema discovery request timeout. Use + /// [`transient_local_replay_timeout`](Self::transient_local_replay_timeout) + /// to configure transient-local replay after the subscriber has been built. + pub fn qos(mut self, qos: QosProfile) -> Self { + self.options = self.options.qos(qos); + self + } + + /// Limit accepted samples by their zenoh origin locality. + /// + /// The locality filter is applied to the dynamic subscriber created after + /// schema discovery succeeds. + pub fn locality(mut self, locality: zenoh::sample::Locality) -> Self { + self.options = self.options.locality(locality); + self + } + + /// Set how long transient-local subscribers wait for replay responses. + /// + /// This timeout is separate from the schema discovery timeout passed to + /// [`crate::node::Node::dynamic_subscriber_auto`]. It only applies when the + /// subscriber QoS requests transient-local durability. + pub fn transient_local_replay_timeout(mut self, timeout: Duration) -> Self { + self.options = self.options.transient_local_replay_timeout(timeout); + self + } + + /// Switch this discovery builder to raw sample delivery. + /// + /// Schema discovery still runs at build time so the subscriber advertises + /// the discovered dynamic type metadata, but received samples are returned + /// without deserialization. + pub fn raw(self) -> DynamicRawSubscriberDiscoveryBuilder { + DynamicRawSubscriberDiscoveryBuilder { + context: self.context, + topic: self.topic, + discovery_timeout: self.discovery_timeout, + options: self.options, + } + } + + /// Discover the topic schema and build the dynamic subscriber. + /// + /// This performs the fallible work deferred by the builder: topic + /// qualification, schema discovery, schema validation, and subscriber + /// declaration. The returned subscriber decodes payloads using the + /// discovered schema. + pub async fn build(self) -> crate::Result { + let Self { + context, + topic, + discovery_timeout, + options, + } = self; + + discover_dynamic_subscriber_builder(context, topic, discovery_timeout, options) + .await? + .build() + .await + } +} + +impl DynamicRawSubscriberDiscoveryBuilder { + /// Set the QoS profile used by the built raw dynamic subscriber. + pub fn qos(mut self, qos: QosProfile) -> Self { + self.options = self.options.qos(qos); + self + } + + /// Limit accepted samples by their zenoh origin locality. + pub fn locality(mut self, locality: zenoh::sample::Locality) -> Self { + self.options = self.options.locality(locality); + self + } + + /// Set how long transient-local subscribers wait for replay responses. + pub fn transient_local_replay_timeout(mut self, timeout: Duration) -> Self { + self.options = self.options.transient_local_replay_timeout(timeout); + self + } + + /// Discover the topic schema and build a raw dynamic subscriber. + pub async fn build(self) -> crate::Result { + let Self { + context, + topic, + discovery_timeout, + options, + } = self; + + discover_dynamic_subscriber_builder(context, topic, discovery_timeout, options) + .await? + .raw() + .build() + .await + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ros-z/src/dynamic/error.rs b/crates/ros-z/src/dynamic/error.rs index d346066115..d7fd10a817 100644 --- a/crates/ros-z/src/dynamic/error.rs +++ b/crates/ros-z/src/dynamic/error.rs @@ -45,7 +45,7 @@ pub enum DynamicError { }, /// Name qualification error. - #[error("failed to qualify schema service name while {operation}: {source}")] + #[error("failed to qualify name while {operation}: {source}")] Name { operation: &'static str, #[source] diff --git a/crates/ros-z/src/dynamic/mod.rs b/crates/ros-z/src/dynamic/mod.rs index 5dc02f9f0e..5d8a32c352 100644 --- a/crates/ros-z/src/dynamic/mod.rs +++ b/crates/ros-z/src/dynamic/mod.rs @@ -81,7 +81,9 @@ mod tests; // Re-export main types pub use codec::{DynamicCdrCodec, DynamicPayload}; -pub use discovery::DiscoveredTopicSchema; +pub use discovery::{ + DiscoveredTopicSchema, DynamicRawSubscriberDiscoveryBuilder, DynamicSubscriberDiscoveryBuilder, +}; pub use error::DynamicError; pub use json::{ ByteRenderPolicy, DynamicJsonRenderPolicy, NonFiniteFloatRenderPolicy, dynamic_payload_to_json, diff --git a/crates/ros-z/src/dynamic/registry.rs b/crates/ros-z/src/dynamic/registry.rs index b2260acf0c..b50073b242 100644 --- a/crates/ros-z/src/dynamic/registry.rs +++ b/crates/ros-z/src/dynamic/registry.rs @@ -51,7 +51,8 @@ impl SchemaRegistry { root_name: &str, schema: Schema, ) -> Result { - let schema_hash = registry_root_schema_hash(root_name, &schema)?; + let schema_hash = + validate_root_schema_identity(root_name, &schema, "registering dynamic schema")?; match self.schemas.get_mut(root_name) { Some(schemas) => { schemas.latest_hash = schema_hash; @@ -105,10 +106,14 @@ impl Default for SchemaRegistry { } } -fn registry_root_schema_hash(root_name: &str, schema: &Schema) -> Result { +pub(crate) fn validate_root_schema_identity( + root_name: &str, + schema: &Schema, + operation: &'static str, +) -> Result { schema .validate() - .map_err(|error| DynamicError::schema("registering dynamic schema", error))?; + .map_err(|error| DynamicError::schema(operation, error))?; let TypeDef::Named(actual_root_name) = &schema.root else { return Err(DynamicError::SerializationError(format!( "schema root for '{root_name}' is not a named type" @@ -116,12 +121,12 @@ fn registry_root_schema_hash(root_name: &str, schema: &Schema) -> Result Result { @@ -95,7 +96,7 @@ fn validate_response_hash( } pub(crate) async fn query_schema( - node: &Node, + context: &EndpointBuilderContext, candidate: &TopicSchemaCandidate, timeout: Duration, ) -> Result<(String, Schema, SchemaHash), DynamicError> { @@ -114,12 +115,14 @@ pub(crate) async fn query_schema( qualify_remote_private_service_name("", &candidate.namespace, &candidate.node_name) .map_err(|error| DynamicError::name("querying remote schema", error))?; - let client = node - .create_service_client::(&service_name) - .map_err(|error| DynamicError::runtime("create schema service client", error))? - .build() - .await - .map_err(|error| DynamicError::runtime("create schema service client", error))?; + let client = ServiceClientBuilder::::new( + context.clone(), + service_name.clone(), + service_endpoint_type::(), + ) + .build() + .await + .map_err(|error| DynamicError::runtime("create schema service client", error))?; let request = build_schema_request(candidate); let response = match client.call_with_timeout_async(&request, timeout).await { diff --git a/crates/ros-z/src/dynamic/schema_service.rs b/crates/ros-z/src/dynamic/schema_service.rs index d867282af7..58fc4d67d9 100644 --- a/crates/ros-z/src/dynamic/schema_service.rs +++ b/crates/ros-z/src/dynamic/schema_service.rs @@ -4,21 +4,19 @@ use std::sync::{Arc, RwLock}; use ros_z_schema::{SchemaBundle, SchemaError, ServiceDef, TypeDef}; use serde::{Deserialize, Serialize}; use tracing::{debug, info, warn}; +use zenoh::Wait; use zenoh::query::Query; -use zenoh::{Result as ZResult, Session, Wait}; use super::error::DynamicError; use super::schema::Schema; use crate::Message; use crate::ServiceTypeInfo; use crate::attachment::Attachment; -use crate::context::GlobalCounter; -use crate::entity::{EndpointEntity, EndpointKind, NodeEntity, SchemaHash, TypeInfo}; -use crate::graph::Graph; +use crate::endpoint_builder::{EndpointBuilderContext, service_endpoint_type}; +use crate::entity::{SchemaHash, TypeInfo}; use crate::message::{SerdeCdrCodec, Service, WireDecoder, WireEncoder}; use crate::schema::{MessageSchema, SchemaBuilder}; use crate::service::{ServiceServer, ServiceServerBuilder}; -use crate::time::Clock; type SchemaVersions = HashMap; type SchemaRegistry = HashMap; @@ -261,57 +259,37 @@ pub struct SchemaService { _server: Arc>, } -#[derive(Clone, Copy)] -pub(crate) struct SchemaServiceNodeIdentity<'a> { - pub(crate) name: &'a str, - pub(crate) namespace: &'a str, - pub(crate) id: usize, +#[derive(Clone)] +pub(crate) struct SchemaRegistrar { + schemas: Arc>, +} + +impl SchemaRegistrar { + pub(crate) fn register_schema( + &self, + root_name: &str, + schema: Schema, + ) -> std::result::Result<(), DynamicError> { + SchemaService::register_registered_schema(&self.schemas, root_name, schema) + } } fn schema_service_server_builder( - session: Session, - node: SchemaServiceNodeIdentity<'_>, - counter: &GlobalCounter, - clock: &Clock, - graph: Arc, + context: EndpointBuilderContext, ) -> ServiceServerBuilder { - let service_name = "~get_schema"; - - let node_entity = NodeEntity::new( - session.zid(), - node.id, - node.name.to_string(), - node.namespace.to_string(), - ); - - let entity = EndpointEntity { - id: counter.increment(), - node: node_entity, - kind: EndpointKind::Service, - topic: service_name.to_string(), - type_info: GetSchema::service_type_info(), - qos: Default::default(), - }; - - ServiceServerBuilder { - entity, - session, - clock: clock.clone(), - graph, - _phantom_data: Default::default(), - } + ServiceServerBuilder::new( + context, + "~get_schema".to_string(), + service_endpoint_type::(), + ) } impl SchemaService { - pub(crate) async fn new( - session: Session, - node: SchemaServiceNodeIdentity<'_>, - counter: &GlobalCounter, - clock: &Clock, - graph: Arc, - ) -> ZResult { + pub(crate) async fn new(context: EndpointBuilderContext) -> crate::Result { let schemas: Arc> = Arc::new(RwLock::new(HashMap::new())); - let server_builder = schema_service_server_builder(session, node, counter, clock, graph); + let node_namespace = context.node.namespace.clone(); + let node_name = context.node.name.clone(); + let server_builder = schema_service_server_builder(context); let schemas_clone = Arc::clone(&schemas); let server = server_builder @@ -322,7 +300,7 @@ impl SchemaService { info!( "[SCH] SchemaService created for node: {}/{}", - node.namespace, node.name + node_namespace, node_name ); Ok(Self { @@ -331,6 +309,12 @@ impl SchemaService { }) } + pub(crate) fn registrar(&self) -> SchemaRegistrar { + SchemaRegistrar { + schemas: Arc::clone(&self.schemas), + } + } + pub fn register_schema( &self, root_name: &str, diff --git a/crates/ros-z/src/endpoint_builder.rs b/crates/ros-z/src/endpoint_builder.rs new file mode 100644 index 0000000000..8a1526b63c --- /dev/null +++ b/crates/ros-z/src/endpoint_builder.rs @@ -0,0 +1,310 @@ +use std::sync::Arc; + +use tracing::debug; +use zenoh::Session; + +use crate::{ + Error, Result, ServiceTypeInfo, + context::GlobalCounter, + dynamic::{ + DynamicError, Schema, registry::validate_root_schema_identity, + schema_service::SchemaRegistrar, + }, + entity::{EndpointEntity, EndpointKind, NodeEntity, TypeInfo}, + error::WireError, + graph::Graph, + message::{Message, Service, validated_type_info_for_schema}, + qos::QosProfile, + shm::ShmConfig, + time::Clock, +}; + +#[derive(Clone)] +pub(crate) struct EndpointBuilderContext { + pub(crate) session: Session, + pub(crate) graph: Arc, + pub(crate) counter: Arc, + pub(crate) node: NodeEntity, + pub(crate) clock: Clock, + pub(crate) shm_config: Option>, + schema_registrar: Option, +} + +impl std::fmt::Debug for EndpointBuilderContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EndpointBuilderContext") + .field("node", &self.node) + .finish_non_exhaustive() + } +} + +impl EndpointBuilderContext { + pub(crate) fn new( + session: Session, + graph: Arc, + counter: Arc, + node: NodeEntity, + clock: Clock, + shm_config: Option>, + schema_registrar: Option, + ) -> Self { + Self { + session, + graph, + counter, + node, + clock, + shm_config, + schema_registrar, + } + } + + pub(crate) fn endpoint_entity( + &self, + kind: EndpointKind, + topic: String, + type_info: TypeInfo, + qos: ros_z_protocol::qos::QosProfile, + ) -> EndpointEntity { + EndpointEntity { + id: self.counter.increment(), + node: self.node.clone(), + kind, + topic, + type_info, + qos, + } + } + + pub(crate) fn register_schema_with_service( + &self, + root_name: &str, + schema: Schema, + ) -> std::result::Result<(), DynamicError> { + if let ros_z_schema::TypeDef::Named(schema_root_name) = &schema.root + && schema_root_name.as_str() != root_name + { + return Err(DynamicError::SerializationError(format!( + "schema root '{}' does not match registered root name '{}'", + schema_root_name.as_str(), + root_name + ))); + } + + if let Some(registrar) = &self.schema_registrar { + registrar.register_schema(root_name, schema)?; + debug!("[NOD] Registered schema {root_name} with schema service"); + } + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) enum MessageEndpointType { + Static { + build: fn() -> StaticMessageMetadata, + }, + Dynamic { + type_info: TypeInfo, + schema: Schema, + validation: DynamicSchemaValidation, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DynamicSchemaValidation { + Required, + Prevalidated, +} + +impl std::fmt::Debug for MessageEndpointType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static { .. } => f.write_str("MessageEndpointType::Static"), + Self::Dynamic { + type_info, + validation, + .. + } => f + .debug_struct("MessageEndpointType::Dynamic") + .field("type_info", type_info) + .field("validation", validation) + .finish_non_exhaustive(), + } + } +} + +impl MessageEndpointType { + pub(crate) fn dynamic(type_info: TypeInfo, schema: Schema) -> Self { + Self::Dynamic { + type_info, + schema, + validation: DynamicSchemaValidation::Required, + } + } + + pub(crate) fn prevalidated_dynamic(type_info: TypeInfo, schema: Schema) -> Self { + Self::Dynamic { + type_info, + schema, + validation: DynamicSchemaValidation::Prevalidated, + } + } +} + +#[derive(Clone)] +pub(crate) struct StaticMessageMetadata { + pub(crate) type_name: String, + pub(crate) type_info: TypeInfo, + pub(crate) schema: Schema, +} + +pub(crate) fn static_message_metadata() -> StaticMessageMetadata +where + T: Message, +{ + let schema = Arc::new(T::schema()); + let type_info = validated_type_info_for_schema::(&schema); + StaticMessageMetadata { + type_name: T::type_name(), + type_info, + schema, + } +} + +impl MessageEndpointType { + fn dynamic_schema_error( + endpoint_kind: &'static str, + topic: &str, + source: DynamicError, + ) -> Error { + Error::from(WireError::DynamicSchema { + endpoint_kind, + topic: topic.to_string(), + source, + }) + } + + fn validate_dynamic_schema_identity( + type_info: &TypeInfo, + schema: &Schema, + ) -> std::result::Result<(), DynamicError> { + let computed_hash = validate_root_schema_identity( + &type_info.name, + schema, + "checking dynamic schema identity", + )?; + if computed_hash != type_info.hash { + return Err(DynamicError::SerializationError(format!( + "schema hash '{}' does not match advertised hash '{}' for '{}'", + computed_hash.to_hash_string(), + type_info.hash.to_hash_string(), + type_info.name + ))); + } + + Ok(()) + } + + fn validate_dynamic_schema( + endpoint_kind: &'static str, + topic: &str, + type_info: &TypeInfo, + schema: &Schema, + validation: DynamicSchemaValidation, + ) -> Result<()> { + if validation == DynamicSchemaValidation::Prevalidated { + return Ok(()); + } + + Self::validate_dynamic_schema_identity(type_info, schema) + .map_err(|source| Self::dynamic_schema_error(endpoint_kind, topic, source)) + } + + pub(crate) fn resolve_for_publisher( + self, + context: &EndpointBuilderContext, + topic: &str, + ) -> Result<(TypeInfo, Option)> { + match self { + Self::Static { build } => { + let metadata = build(); + context + .register_schema_with_service(&metadata.type_name, Arc::clone(&metadata.schema)) + .map_err(|source| { + Error::from(WireError::DynamicSchema { + endpoint_kind: "publisher", + topic: topic.to_string(), + source, + }) + })?; + Ok((metadata.type_info, None)) + } + Self::Dynamic { + type_info, + schema, + validation, + } => { + Self::validate_dynamic_schema("publisher", topic, &type_info, &schema, validation)?; + context + .register_schema_with_service(&type_info.name, Arc::clone(&schema)) + .map_err(|source| Self::dynamic_schema_error("publisher", topic, source))?; + Ok((type_info, Some(schema))) + } + } + } + + pub(crate) fn resolve_for_subscriber(self, topic: &str) -> Result<(TypeInfo, Option)> { + match self { + Self::Static { build } => { + let metadata = build(); + Ok((metadata.type_info, None)) + } + Self::Dynamic { + type_info, + schema, + validation, + } => { + Self::validate_dynamic_schema( + "subscriber", + topic, + &type_info, + &schema, + validation, + )?; + Ok((type_info, Some(schema))) + } + } + } +} + +#[derive(Clone)] +pub(crate) struct ServiceEndpointType { + build: fn() -> TypeInfo, +} + +impl std::fmt::Debug for ServiceEndpointType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("ServiceEndpointType") + } +} + +pub(crate) fn service_endpoint_type() -> ServiceEndpointType +where + T: Service + ServiceTypeInfo, +{ + ServiceEndpointType { + build: T::service_type_info, + } +} + +impl ServiceEndpointType { + pub(crate) fn resolve(&self) -> TypeInfo { + (self.build)() + } +} + +pub(crate) fn default_protocol_qos() -> ros_z_protocol::qos::QosProfile { + QosProfile::default().to_protocol_qos() +} diff --git a/crates/ros-z/src/error.rs b/crates/ros-z/src/error.rs index 98220f50af..a4c7bdb569 100644 --- a/crates/ros-z/src/error.rs +++ b/crates/ros-z/src/error.rs @@ -345,17 +345,4 @@ impl Error { } .into() } - - pub(crate) fn schema( - endpoint_kind: &'static str, - topic: impl Into, - source: ros_z_schema::SchemaError, - ) -> Self { - WireError::Schema { - endpoint_kind, - topic: topic.into(), - source, - } - .into() - } } diff --git a/crates/ros-z/src/lib.rs b/crates/ros-z/src/lib.rs index f6a5c51b87..906e3a8b93 100644 --- a/crates/ros-z/src/lib.rs +++ b/crates/ros-z/src/lib.rs @@ -10,10 +10,20 @@ //! //! let context = ContextBuilder::default().build().await?; //! let node = context.create_node("talker").build().await?; -//! let publisher = node.publisher::("/chatter")?.build().await?; +//! let publisher = node.publisher::("/chatter").build().await?; //! publisher.publish(&"hello".to_owned()).await?; //! ``` //! +//! ## Endpoint builders +//! +//! ```rust,ignore +//! let publisher = node.publisher::("/chatter").build().await?; +//! let subscriber = node.subscriber::("/chatter").build().await?; +//! let cache = node.subscriber::("/chatter").cache(200).build().await?; +//! let server = node.service_server::("add_two_ints").build().await?; +//! let client = node.service_client::("add_two_ints").build().await?; +//! ``` +//! //! ## Sync and async APIs //! //! Runtime-resource builders (context, node, pub/sub, services, and @@ -49,6 +59,7 @@ pub mod context; /// Dynamic (schema-less) message support. pub mod dynamic; pub mod encoding; +mod endpoint_builder; /// Entity identity types (`SchemaHash`, `TypeInfo`). pub mod entity; pub mod error; diff --git a/crates/ros-z/src/node.rs b/crates/ros-z/src/node.rs index f235d5df13..d004292e5e 100644 --- a/crates/ros-z/src/node.rs +++ b/crates/ros-z/src/node.rs @@ -2,18 +2,18 @@ use std::{sync::Arc, time::Duration}; use crate::{ Error, Result, ServiceTypeInfo, - cache::CacheBuilder, context::{GlobalCounter, RuntimeParameterInputs}, dynamic::{ - DiscoveredTopicSchema, DynamicCdrCodec, DynamicError, DynamicPayload, - DynamicPublisherBuilder, DynamicSubscriberBuilder, Schema, SchemaDiscovery, SchemaService, - schema_service::SchemaServiceNodeIdentity, + DiscoveredTopicSchema, DynamicError, DynamicPublisherBuilder, DynamicSubscriberBuilder, + DynamicSubscriberDiscoveryBuilder, Schema, SchemaDiscovery, SchemaService, + }, + endpoint_builder::{ + EndpointBuilderContext, MessageEndpointType, service_endpoint_type, static_message_metadata, }, entity::*, - error::WireError, graph::Graph, - message::{Message, Service, WireDecoder, WireEncoder, validated_type_info_for_schema}, - pubsub::{DEFAULT_TRANSIENT_LOCAL_REPLAY_TIMEOUT, PublisherBuilder, SubscriberBuilder}, + message::{Message, Service, WireDecoder, WireEncoder}, + pubsub::{PublisherBuilder, SubscriberBuilder}, service::{ServiceClientBuilder, ServiceServerBuilder}, shm::ShmConfig, time::{Clock, Timer}, @@ -182,19 +182,16 @@ impl NodeBuilder { // Create schema service if enabled let schema_service = if self.enable_schema_service { debug!("[NOD] Creating schema service"); - let service = SchemaService::new( + let schema_context = EndpointBuilderContext::new( self.session.clone(), - SchemaServiceNodeIdentity { - name: &self.name, - namespace: &self.namespace, - id, - }, - &self.counter, - &self.clock, self.graph.clone(), - ) - .await - .map_err(|source| Error::zenoh("create schema service", source))?; + self.counter.clone(), + node.clone(), + self.clock.clone(), + self.shm_config.clone(), + None, + ); + let service = SchemaService::new(schema_context).await?; info!("[NOD] SchemaService created (callback mode)"); @@ -235,52 +232,20 @@ impl Node { /// # Panics /// /// Panics if `T` builds an invalid static [`Message`] schema or schema hash. - /// Runtime and dynamic schema registration failures are returned as errors. - pub fn publisher(&self, topic: &str) -> Result> + /// Runtime and dynamic schema registration failures are returned by build. + pub fn publisher(&self, topic: &str) -> PublisherBuilder where T: Message, T::Codec: WireEncoder, { - debug!("[NOD] Creating publisher: topic={}", topic); - - let schema = Arc::new(T::schema()); - let type_info = validated_type_info_for_schema::(&schema); - let type_name = T::type_name(); - self.register_schema_with_service(&type_name, Arc::clone(&schema)) - .map_err(|error| { - Error::from(WireError::DynamicSchema { - endpoint_kind: "publisher", - topic: topic.to_string(), - source: error, - }) - })?; - - Ok(self.publisher_impl::(topic, type_info)) - } - - fn publisher_impl(&self, topic: &str, type_info: TypeInfo) -> PublisherBuilder - where - S: WireEncoder, - { - // Note: Topic qualification happens in PublisherBuilder::build() - // to allow error handling in the Result type - let entity = EndpointEntity { - id: self.counter.increment(), - node: self.entity.clone(), - kind: EndpointKind::Publisher, - topic: topic.to_string(), - type_info, - qos: Default::default(), - }; - PublisherBuilder { - entity, - session: self.session.clone(), - graph: self.graph.clone(), - clock: self.clock.clone(), - shm_config: self.shm_config.clone(), - dyn_schema: None, - _phantom_data: Default::default(), - } + debug!("[NOD] Creating publisher builder: topic={}", topic); + PublisherBuilder::new( + self.endpoint_builder_context(), + topic.to_string(), + MessageEndpointType::Static { + build: static_message_metadata::, + }, + ) } /// Create a subscriber for the given topic. @@ -296,99 +261,19 @@ impl Node { /// /// Panics if `T` builds an invalid static [`Message`] schema or schema hash. /// Runtime and dynamic subscriber failures are returned as errors. - pub fn subscriber(&self, topic: &str) -> Result> + pub fn subscriber(&self, topic: &str) -> SubscriberBuilder where T: Message, T::Codec: WireDecoder, { - debug!("[NOD] Creating subscriber: topic={}", topic); - - let schema = T::schema(); - let type_info = validated_type_info_for_schema::(&schema); - - Ok(self.subscriber_impl::(topic, type_info)) - } - - fn subscriber_impl(&self, topic: &str, type_info: TypeInfo) -> SubscriberBuilder - where - S: WireDecoder, - { - // Note: Topic qualification happens in SubscriberBuilder::build() - // to allow error handling in the Result type - let entity = EndpointEntity { - id: self.counter.increment(), - node: self.entity.clone(), - kind: EndpointKind::Subscription, - topic: topic.to_string(), - type_info, - qos: Default::default(), - }; - SubscriberBuilder { - entity, - session: self.session.clone(), - graph: self.graph.clone(), - dyn_schema: None, - locality: None, - transient_local_replay_timeout: DEFAULT_TRANSIENT_LOCAL_REPLAY_TIMEOUT, - _phantom_data: Default::default(), - } - } - - /// Create a timestamp-indexed sliding-window cache subscriber for `topic`, - /// retaining up to `capacity` messages. - /// - /// A capacity of `0` disables retention and stores no messages. - /// - /// By default, messages are indexed by the Zenoh transport timestamp - /// (zero-config, works for any message type). Call - /// [`.with_stamp(|message| ...)`](CacheBuilder::with_stamp) on the returned - /// builder to switch to application-level timestamp extraction (e.g. - /// `header.stamp`). - /// - /// # Example - /// - /// ```rust,ignore - /// use ros_z::prelude::*; - /// use ros_z::time::Time; - /// use std::time::Duration; - /// - /// # async fn example() -> ros_z::Result<()> { - /// let context = ContextBuilder::default().build().await?; - /// let node = context.create_node("cache_demo").build().await?; - /// - /// // Zero-config (Zenoh transport timestamp) - /// let cache = node.create_cache::("/chatter", 200)?.build().await?; - /// - /// // Pull messages from the last 100 ms - /// let now = Time::from_wallclock(std::time::SystemTime::now()); - /// let msgs = cache.get_interval(now - Duration::from_millis(100), now); - /// # Ok(()) - /// # } - /// ``` - /// - /// # Panics - /// - /// Panics if `T` builds an invalid static [`Message`] schema or schema hash. - /// Runtime cache/subscriber failures are returned as errors. - pub fn create_cache( - &self, - topic: &str, - capacity: usize, - ) -> Result::Codec>> - where - T: Message, - for<'a> ::Codec: WireDecoder = &'a [u8], Output = T>, - { - debug!( - "[NOD] Creating cache: topic={}, capacity={}", - topic, capacity - ); - let schema = T::schema(); - let type_info = validated_type_info_for_schema::(&schema); - Ok(CacheBuilder::new( - self.subscriber_impl::::Codec>(topic, type_info), - capacity, - )) + debug!("[NOD] Creating subscriber builder: topic={}", topic); + SubscriberBuilder::new( + self.endpoint_builder_context(), + topic.to_string(), + MessageEndpointType::Static { + build: static_message_metadata::, + }, + ) } /// Create a periodic timer tied to this node's clock. @@ -414,38 +299,16 @@ impl Node { /// Panics if `T` builds invalid static service type metadata, or if /// `T::Request` or `T::Response` builds an invalid static [`Message`] /// schema or schema hash. - pub fn create_service_server(&self, name: &str) -> Result> + pub fn service_server(&self, name: &str) -> ServiceServerBuilder where T: Service + ServiceTypeInfo, { - debug!("[NOD] Creating service server: name={}", name); - let type_info = T::service_type_info(); - Ok(self.create_service_impl(name, type_info)) - } - - #[doc(hidden)] - pub fn create_service_impl( - &self, - name: &str, - type_info: TypeInfo, - ) -> ServiceServerBuilder { - // Note: Service name qualification happens in ServiceServerBuilder::build() - // to allow error handling in the Result type - let entity = EndpointEntity { - id: self.counter.increment(), - node: self.entity.clone(), - kind: EndpointKind::Service, - topic: name.to_string(), - type_info, - qos: Default::default(), - }; - ServiceServerBuilder { - entity, - session: self.session.clone(), - clock: self.clock.clone(), - graph: self.graph.clone(), - _phantom_data: Default::default(), - } + debug!("[NOD] Creating service server builder: name={}", name); + ServiceServerBuilder::new( + self.endpoint_builder_context(), + name.to_string(), + service_endpoint_type::(), + ) } /// Create a typed service client builder for `name`. @@ -463,38 +326,16 @@ impl Node { /// Panics if `T` builds invalid static service type metadata, or if /// `T::Request` or `T::Response` builds an invalid static [`Message`] /// schema or schema hash. - pub fn create_service_client(&self, name: &str) -> Result> + pub fn service_client(&self, name: &str) -> ServiceClientBuilder where T: Service + ServiceTypeInfo, { - debug!("[NOD] Creating service client: name={}", name); - let type_info = T::service_type_info(); - Ok(self.create_client_impl(name, type_info)) - } - - #[doc(hidden)] - pub fn create_client_impl( - &self, - name: &str, - type_info: TypeInfo, - ) -> ServiceClientBuilder { - // Note: Service name qualification happens in ServiceClientBuilder::build() - // to allow error handling in the Result type - let entity = EndpointEntity { - id: self.counter.increment(), - node: self.entity.clone(), - kind: EndpointKind::Client, - topic: name.to_string(), - type_info, - qos: Default::default(), - }; - ServiceClientBuilder { - entity, - session: self.session.clone(), - clock: self.clock.clone(), - graph: self.graph.clone(), - _phantom_data: Default::default(), - } + debug!("[NOD] Creating service client builder: name={}", name); + ServiceClientBuilder::new( + self.endpoint_builder_context(), + name.to_string(), + service_endpoint_type::(), + ) } /// Get a reference to this node's schema service, if enabled. @@ -562,19 +403,33 @@ impl Node { &self.clock } + fn endpoint_builder_context(&self) -> EndpointBuilderContext { + EndpointBuilderContext::new( + self.session.clone(), + self.graph.clone(), + self.counter.clone(), + self.entity.clone(), + self.clock.clone(), + self.shm_config.clone(), + self.schema_service().map(|service| service.registrar()), + ) + } + // ======================================================================== // Dynamic Message API // ======================================================================== - /// Create a dynamic publisher for the given topic. + /// Create a dynamic publisher builder for the given topic. /// - /// If this node has a schema service enabled, the schema will be - /// automatically registered, allowing other nodes to discover it via the - /// `GetSchema` service. + /// This returns an infallible builder immediately. Topic qualification, + /// schema validation, type/hash checks, publisher declaration, and automatic + /// schema-service registration happen when the builder is built with + /// `.build().await`. /// /// # Arguments /// /// * `topic` - The topic name to publish on + /// * `type_info` - The dynamic message type name and schema hash to advertise /// * `schema` - The message schema for serialization /// /// # Example @@ -592,9 +447,12 @@ impl Node { /// }); /// /// let type_info = TypeInfo::new("std_msgs::String", ros_z_schema::compute_hash(schema.as_ref())?); - /// let publisher = node.dynamic_publisher("chatter", type_info, schema)?.build().await?; + /// let publisher = node + /// .dynamic_publisher("chatter", type_info, schema.clone()) + /// .build() + /// .await?; /// - /// let mut message = DynamicStruct::default_for_schema(publisher.schema().unwrap())?; + /// let mut message = DynamicStruct::default_for_schema(&schema)?; /// message.set("data", "Hello, world!")?; /// let payload = DynamicPayload::from_struct(message)?; /// publisher.publish(&payload).await?; @@ -604,23 +462,13 @@ impl Node { topic: &str, type_info: TypeInfo, schema: Schema, - ) -> Result { - schema - .validate() - .map_err(|error| Error::schema("publisher", topic, error))?; - - self.register_schema_with_service(&type_info.name, Arc::clone(&schema)) - .map_err(|source| { - Error::from(WireError::DynamicSchema { - endpoint_kind: "publisher", - topic: topic.to_string(), - source, - }) - })?; - - Ok(self - .publisher_impl::(topic, type_info) - .dynamic_schema(schema)) + ) -> DynamicPublisherBuilder { + debug!("[NOD] Creating dynamic publisher builder: topic={}", topic); + PublisherBuilder::new( + self.endpoint_builder_context(), + topic.to_string(), + MessageEndpointType::dynamic(type_info, schema), + ) } /// Discover the schema that publishers currently expose on a topic. @@ -632,17 +480,17 @@ impl Node { topic: &str, discovery_timeout: Duration, ) -> std::result::Result { - SchemaDiscovery::new(self, discovery_timeout) + SchemaDiscovery::new(self.endpoint_builder_context(), discovery_timeout) .discover(topic) .await } - /// Create a dynamic subscriber with automatic schema discovery. + /// Create a dynamic subscriber builder with automatic schema discovery. /// - /// This method queries publishers on the topic for their schema service - /// and returns a preconfigured subscriber builder using the discovered - /// schema. This is useful when you don't know the message type at compile - /// time. + /// This method returns a discovery builder immediately. When you build it, + /// the builder queries publishers on the topic for their schema service and + /// creates a dynamic subscriber from the discovered schema. This is useful + /// when you don't know the message type at compile time. /// /// The topic name will be qualified as a ros-z graph name: /// - Absolute topics (starting with '/') are used as-is @@ -656,57 +504,58 @@ impl Node { /// /// # Returns /// - /// A preconfigured dynamic subscriber builder on success. + /// A dynamic subscriber discovery builder. Schema discovery runs when the + /// builder is built. /// /// # Example /// /// ```ignore - /// // Discover schema from publishers and create subscriber + /// // Discover schema from publishers and create the subscriber at build time. /// let subscriber = node.dynamic_subscriber_auto( /// "chatter", /// Duration::from_secs(5), - /// ).await? + /// ) /// .build() /// .await?; /// - /// println!("Discovered type: {}", subscriber.schema().unwrap().type_name); + /// println!("Discovered schema root: {:?}", subscriber.schema().unwrap().root); /// /// // Receive messages - /// let message = subscriber.recv().await?; - /// let data: String = message.get("data")?; + /// let payload = subscriber.recv().await?; + /// if let ros_z::dynamic::DynamicValue::Struct(message) = payload.value { + /// let data: String = message.get("data")?; + /// println!("received: {data}"); + /// } /// ``` - pub async fn dynamic_subscriber_auto( + pub fn dynamic_subscriber_auto( &self, topic: &str, discovery_timeout: Duration, - ) -> Result { + ) -> DynamicSubscriberDiscoveryBuilder { debug!( "[NOD] Creating dynamic subscriber with auto-discovery for topic: {}", topic ); - let discovered = self.discover_topic_schema(topic, discovery_timeout).await?; - - info!( - "[NOD] Discovered schema for topic {}: {} (hash: {})", - discovered.qualified_topic, - discovered.root_name, - discovered.schema_hash.to_hash_string() - ); - - Ok(self - .subscriber_impl::(topic, discovered.type_info()) - .dynamic_schema(discovered.schema)) + DynamicSubscriberDiscoveryBuilder::new( + self.endpoint_builder_context(), + topic.to_string(), + discovery_timeout, + ) } - /// Create a dynamic subscriber with a known schema. + /// Create a dynamic subscriber builder with a known schema. /// /// Use this when you already have the schema (e.g., loaded from a file - /// or built programmatically). + /// or built programmatically). This returns an infallible builder + /// immediately. Topic qualification, schema validation, type/hash checks, + /// and subscriber declaration happen when the builder is built with + /// `.build().await`. /// /// # Arguments /// /// * `topic` - The topic name to subscribe to + /// * `type_info` - The dynamic message type name and schema hash expected on the topic /// * `schema` - The message schema for deserialization /// /// The topic name will be qualified as a ros-z graph name: @@ -729,22 +578,24 @@ impl Node { /// }); /// /// let type_info = TypeInfo::new("std_msgs::String", ros_z_schema::compute_hash(schema.as_ref())?); - /// let subscriber = node.dynamic_subscriber("chatter", type_info, schema)?.build().await?; - /// let message = subscriber.recv().await?; + /// let subscriber = node.dynamic_subscriber("chatter", type_info, schema).build().await?; + /// let payload = subscriber.recv().await?; + /// if let ros_z::dynamic::DynamicValue::Struct(message) = payload.value { + /// let data: String = message.get("data")?; + /// println!("received: {data}"); + /// } /// ``` pub fn dynamic_subscriber( &self, topic: &str, type_info: TypeInfo, schema: Schema, - ) -> Result { - if let Err(error) = schema.validate() { - return Err(Error::schema("subscriber", topic, error)); - } - - Ok(self - .subscriber_impl::(topic, type_info) - .dynamic_schema(schema)) + ) -> DynamicSubscriberBuilder { + SubscriberBuilder::new( + self.endpoint_builder_context(), + topic.to_string(), + MessageEndpointType::dynamic(type_info, schema), + ) } pub fn register_schema_with_service( @@ -752,20 +603,7 @@ impl Node { root_name: &str, schema: Schema, ) -> std::result::Result<(), DynamicError> { - if let ros_z_schema::TypeDef::Named(schema_root_name) = &schema.root - && schema_root_name.as_str() != root_name - { - return Err(DynamicError::SerializationError(format!( - "schema root '{}' does not match registered root name '{}'", - schema_root_name.as_str(), - root_name - ))); - } - if let Some(service) = &self.schema_service { - service.register_schema(root_name, schema)?; - debug!("[NOD] Registered schema {root_name} with schema service"); - } - - Ok(()) + self.endpoint_builder_context() + .register_schema_with_service(root_name, schema) } } diff --git a/crates/ros-z/src/parameter/remote/client.rs b/crates/ros-z/src/parameter/remote/client.rs index c43f679d8e..cd871ef7e5 100644 --- a/crates/ros-z/src/parameter/remote/client.rs +++ b/crates/ros-z/src/parameter/remote/client.rs @@ -151,7 +151,6 @@ impl RemoteParameterClient { let operation = format!("subscribing to remote parameter events '{events_topic}'"); self.node .subscriber::(&events_topic) - .map_err(|source| ParameterError::operation(operation.clone(), source))? .qos(QosProfile { reliability: QosReliability::Reliable, durability: QosDurability::TransientLocal, @@ -187,8 +186,7 @@ impl RemoteParameterClient { { let operation = format!("creating remote parameter service client '{service_name}'"); self.node - .create_service_client::(service_name) - .map_err(|source| ParameterError::operation(operation.clone(), source))? + .service_client::(service_name) .build() .await .map_err(|source| ParameterError::operation(operation, source)) diff --git a/crates/ros-z/src/parameter/remote/services.rs b/crates/ros-z/src/parameter/remote/services.rs index 6ffa27b8fb..a32796728d 100644 --- a/crates/ros-z/src/parameter/remote/services.rs +++ b/crates/ros-z/src/parameter/remote/services.rs @@ -37,9 +37,6 @@ where pub async fn register(node: &Node, inner: Arc>) -> Result { let event_publisher = node .publisher::("~parameter/events") - .map_err(|err| ParameterError::RemoteError { - message: err.to_string(), - })? .qos(QosProfile { reliability: QosReliability::Reliable, durability: QosDurability::TransientLocal, @@ -128,10 +125,7 @@ where S: Service + ServiceTypeInfo, { let operation = format!("creating parameter service server '{name}'"); - node.create_service_server::(name) - .map_err(|err| ParameterError::RemoteError { - message: err.to_string(), - })? + node.service_server::(name) .build_with_callback(move |query| handler(&query)) .await .map_err(|source| ParameterError::operation(operation, source)) diff --git a/crates/ros-z/src/pubsub.rs b/crates/ros-z/src/pubsub.rs index 5e91b88812..e4c175d5f4 100644 --- a/crates/ros-z/src/pubsub.rs +++ b/crates/ros-z/src/pubsub.rs @@ -9,6 +9,7 @@ mod subscriber; pub use metadata::{PublicationId, Received}; pub use publisher::{PreparedPublication, Publisher, PublisherBuilder}; pub use raw::{RawSubscriber, RawSubscriberBuilder}; +pub(crate) use subscriber::SubscriberOptions; pub use subscriber::{Subscriber, SubscriberBuilder}; pub(crate) const DEFAULT_TRANSIENT_LOCAL_REPLAY_TIMEOUT: Duration = Duration::from_secs(1); diff --git a/crates/ros-z/src/pubsub/publisher.rs b/crates/ros-z/src/pubsub/publisher.rs index bf061ac6b8..466a9ebf1b 100644 --- a/crates/ros-z/src/pubsub/publisher.rs +++ b/crates/ros-z/src/pubsub/publisher.rs @@ -13,7 +13,8 @@ use crate::Result; use crate::attachment::{Attachment, EndpointGlobalId}; use crate::dynamic::{DynamicCdrCodec, DynamicPayload, Schema}; use crate::encoding::Encoding; -use crate::entity::EndpointEntity; +use crate::endpoint_builder::{EndpointBuilderContext, MessageEndpointType}; +use crate::entity::{EndpointEntity, EndpointKind}; use crate::graph::Graph; use crate::message::WireEncoder; use crate::pubsub::metadata::PublicationId; @@ -89,6 +90,37 @@ impl Drop for ReplayTaskGuard { } } +struct PreparedPublisherBuild { + session: Session, + graph: Arc, + clock: Clock, + shm_config: Option>, + entity: EndpointEntity, + key_expr: zenoh::key_expr::KeyExpr<'static>, + topic_key_expr: ros_z_protocol::entity::TopicKE, + transient_local_cache: Option>, + endpoint_global_id: EndpointGlobalId, + dyn_schema: Option, +} + +impl PreparedPublisherBuild { + fn warn_about_incompatible_endpoints(graph: &Graph, entity: &EndpointEntity) { + for endpoint in graph.type_incompatible_endpoints_for(entity) { + warn!( + topic = %entity.topic, + publisher_node = %entity.node.fully_qualified_name(), + publisher_type = %entity.type_info.name, + publisher_schema_hash = %entity.type_info.hash, + endpoint_kind = ?endpoint.kind, + endpoint_node = %endpoint.node.fully_qualified_name(), + endpoint_type = %endpoint.type_info.name, + endpoint_schema_hash = %endpoint.type_info.hash, + "[PUB] endpoint type metadata does not match publisher" + ); + } + } +} + async fn spawn_transient_local_replay_queryable( session: &Session, topic_key_expr: &ros_z_protocol::entity::TopicKE, @@ -161,20 +193,33 @@ impl std::fmt::Debug for Publisher { #[derive(Debug)] pub struct PublisherBuilder::Codec> { - pub(crate) entity: EndpointEntity, - pub(crate) session: Session, - pub(crate) graph: Arc, - pub(crate) clock: Clock, + pub(crate) context: EndpointBuilderContext, + pub(crate) topic: String, + pub(crate) type_source: MessageEndpointType, + pub(crate) qos: ros_z_protocol::qos::QosProfile, pub(crate) shm_config: Option>, - /// Schema for dynamic message publishing. - /// When set, the schema will be registered with the schema service. - pub(crate) dyn_schema: Option, pub(crate) _phantom_data: PhantomData<(T, C)>, } impl PublisherBuilder { + pub(crate) fn new( + context: EndpointBuilderContext, + topic: String, + type_source: MessageEndpointType, + ) -> Self { + let shm_config = context.shm_config.clone(); + Self { + context, + topic, + type_source, + qos: crate::endpoint_builder::default_protocol_qos(), + shm_config, + _phantom_data: Default::default(), + } + } + pub fn qos(mut self, qos: QosProfile) -> Self { - self.entity.qos = qos.to_protocol_qos(); + self.qos = qos.to_protocol_qos(); self } @@ -194,7 +239,7 @@ impl PublisherBuilder { /// let provider = Arc::new(ShmProviderBuilder::new(20 * 1024 * 1024).build()?); /// let config = ShmConfig::new(provider).with_threshold(5_000); /// - /// let publisher = node.publisher::("topic")? + /// let publisher = node.publisher::("topic") /// .shm_config(config) /// .build() /// .await?; @@ -218,7 +263,7 @@ impl PublisherBuilder { /// # let context = ros_z::context::ContextBuilder::default().with_shm_enabled()?.build().await?; /// # let node = context.create_node("test").build().await?; /// // Context has SHM enabled, but disable for this publisher - /// let publisher = node.publisher::("small_messages")? + /// let publisher = node.publisher::("small_messages") /// .without_shm() /// .build() /// .await?; @@ -229,12 +274,6 @@ impl PublisherBuilder { self.shm_config = None; self } - - /// Set the dynamic schema for runtime-typed publishers. - pub fn dynamic_schema(mut self, schema: Schema) -> Self { - self.dyn_schema = Some(schema); - self - } } impl PublisherBuilder @@ -243,38 +282,40 @@ where C: for<'a> WireEncoder = &'a T> + 'static, { #[tracing::instrument(name = "pub_build", skip(self), fields( - topic = %self.entity.topic + topic = %self.topic ))] pub async fn build(self) -> Result> { self.build_inner_async().await } - fn prepare_build( - &mut self, - ) -> Result<( - zenoh::key_expr::KeyExpr<'static>, - ros_z_protocol::entity::TopicKE, - Option>, - EndpointGlobalId, - )> { + fn prepare_build(self) -> Result { + let (type_info, dyn_schema) = self + .type_source + .resolve_for_publisher(&self.context, &self.topic)?; + // Qualify the topic name as a ros-z graph name. - let topic = self.entity.topic.clone(); + let topic = self.topic; let qualified_topic = topic_name::qualify_topic_name( &topic, - &self.entity.node.namespace, - &self.entity.node.name, + &self.context.node.namespace, + &self.context.node.name, ) .map_err(|source| crate::Error::topic_name(topic, source))?; - self.entity.topic = qualified_topic.clone(); debug!("[PUB] Qualified topic: {}", qualified_topic); - let topic_key_expr = ros_z_protocol::format::topic_key_expr(&self.entity)?; - let data_key_expr = (*topic_key_expr).clone(); - debug!("[PUB] Key expression: {}", data_key_expr); + let entity = self.context.endpoint_entity( + EndpointKind::Publisher, + qualified_topic, + type_info, + self.qos, + ); + let topic_key_expr = ros_z_protocol::format::topic_key_expr(&entity)?; + let key_expr = (*topic_key_expr).clone(); + debug!("[PUB] Key expression: {}", key_expr); if matches!( - self.entity.qos, + entity.qos, ros_z_protocol::qos::QosProfile { durability: QosDurability::TransientLocal, history: QosHistory::KeepAll, @@ -286,41 +327,30 @@ where ); } - let transient_local_cache = replay::transient_local_cache_capacity(&self.entity.qos) + let transient_local_cache = replay::transient_local_cache_capacity(&entity.qos) .map(|capacity| Arc::new(TransientLocalCache::new(capacity))); - let endpoint_global_id = EndpointGlobalId::from(&self.entity); + let endpoint_global_id = EndpointGlobalId::from(&entity); - Ok(( - data_key_expr, + Ok(PreparedPublisherBuild { + session: self.context.session.clone(), + graph: self.context.graph.clone(), + clock: self.context.clock.clone(), + shm_config: self.shm_config, + entity, + key_expr, topic_key_expr, transient_local_cache, endpoint_global_id, - )) - } - - fn warn_about_incompatible_endpoints(&self) { - for endpoint in self.graph.type_incompatible_endpoints_for(&self.entity) { - warn!( - topic = %self.entity.topic, - publisher_node = %self.entity.node.fully_qualified_name(), - publisher_type = %self.entity.type_info.name, - publisher_schema_hash = %self.entity.type_info.hash, - endpoint_kind = ?endpoint.kind, - endpoint_node = %endpoint.node.fully_qualified_name(), - endpoint_type = %endpoint.type_info.name, - endpoint_schema_hash = %endpoint.type_info.hash, - "[PUB] endpoint type metadata does not match publisher" - ); - } + dyn_schema, + }) } - async fn build_inner_async(mut self) -> Result> { - let (key_expr, topic_key_expr, transient_local_cache, endpoint_global_id) = - self.prepare_build()?; + async fn build_inner_async(self) -> Result> { + let prepared = self.prepare_build()?; - let mut pub_builder = self.session.declare_publisher(key_expr); + let mut pub_builder = prepared.session.declare_publisher(prepared.key_expr); - match self.entity.qos.reliability { + match prepared.entity.qos.reliability { QosReliability::Reliable => { pub_builder = pub_builder.congestion_control(zenoh::qos::CongestionControl::Block); debug!("[PUB] QoS: Reliable (Block)"); @@ -331,28 +361,29 @@ where } } - let transient_local_replay_task = if let Some(cache) = transient_local_cache.as_ref() { - Some( - spawn_transient_local_replay_queryable( - &self.session, - &topic_key_expr, - endpoint_global_id, - cache.clone(), + let transient_local_replay_task = + if let Some(cache) = prepared.transient_local_cache.as_ref() { + Some( + spawn_transient_local_replay_queryable( + &prepared.session, + &prepared.topic_key_expr, + prepared.endpoint_global_id, + cache.clone(), + ) + .await?, ) - .await?, - ) - } else { - None - }; + } else { + None + }; let transient_local_replay_task = ReplayTaskGuard::new(transient_local_replay_task); let inner = pub_builder .await .map_err(|source| crate::Error::zenoh("declare publisher", source))?; - debug!("[PUB] Publisher ready: topic={}", self.entity.topic); + debug!("[PUB] Publisher ready: topic={}", prepared.entity.topic); - let liveliness_key_expr = self.entity.liveliness_key_expr()?.0; - let lv_token = self + let liveliness_key_expr = prepared.entity.liveliness_key_expr()?.0; + let lv_token = prepared .session .liveliness() .declare_token(liveliness_key_expr) @@ -360,20 +391,23 @@ where .map_err(|source| crate::Error::zenoh("declare publisher liveliness token", source))?; let encoding = Arc::new(Encoding::cdr().to_zenoh_encoding()); debug!("[PUB] Using encoding: {}", encoding); - self.warn_about_incompatible_endpoints(); + PreparedPublisherBuild::warn_about_incompatible_endpoints( + &prepared.graph, + &prepared.entity, + ); Ok(Publisher { - entity: self.entity, + entity: prepared.entity, sequence_number: AtomicUsize::new(0), inner, _lv_token: lv_token, - endpoint_global_id, - clock: self.clock, - shm_config: self.shm_config, - dyn_schema: self.dyn_schema, + endpoint_global_id: prepared.endpoint_global_id, + clock: prepared.clock, + shm_config: prepared.shm_config, + dyn_schema: prepared.dyn_schema, encoding, - graph: self.graph, - transient_local_cache, + graph: prepared.graph, + transient_local_cache: prepared.transient_local_cache, transient_local_replay_task: transient_local_replay_task.into_task(), _phantom_data: Default::default(), }) @@ -606,7 +640,7 @@ impl Publisher { /// Get the dynamic schema used by this publisher. /// /// Returns `None` if the publisher was not created through - /// `Node::dynamic_publisher` or with `.dynamic_schema()`. + /// `Node::dynamic_publisher`. pub fn schema(&self) -> Option<&SchemaBundle> { self.dyn_schema.as_ref().map(|s| s.as_ref()) } diff --git a/crates/ros-z/src/pubsub/subscriber.rs b/crates/ros-z/src/pubsub/subscriber.rs index fdac0ae2a7..d1877bd6f4 100644 --- a/crates/ros-z/src/pubsub/subscriber.rs +++ b/crates/ros-z/src/pubsub/subscriber.rs @@ -9,7 +9,8 @@ use zenoh::{Session, sample::Sample}; use crate::Result; use crate::dynamic::{DynamicCdrCodec, DynamicPayload, Schema}; -use crate::entity::EndpointEntity; +use crate::endpoint_builder::{EndpointBuilderContext, MessageEndpointType}; +use crate::entity::{EndpointEntity, EndpointKind}; use crate::graph::Graph; use crate::message::WireDecoder; use crate::pubsub::metadata::Received; @@ -27,13 +28,45 @@ pub(super) fn subscriber_queue_capacity(qos: &ros_z_protocol::qos::QosProfile) - } } -pub struct SubscriberBuilder::Codec> { - pub(crate) entity: EndpointEntity, - pub(crate) session: Session, - pub(crate) graph: Arc, - pub(crate) dyn_schema: Option, +#[derive(Debug, Clone)] +pub(crate) struct SubscriberOptions { + pub(crate) qos: ros_z_protocol::qos::QosProfile, pub(crate) locality: Option, pub(crate) transient_local_replay_timeout: Duration, +} + +impl Default for SubscriberOptions { + fn default() -> Self { + Self { + qos: crate::endpoint_builder::default_protocol_qos(), + locality: None, + transient_local_replay_timeout: crate::pubsub::DEFAULT_TRANSIENT_LOCAL_REPLAY_TIMEOUT, + } + } +} + +impl SubscriberOptions { + pub(crate) fn qos(mut self, qos: QosProfile) -> Self { + self.qos = qos.to_protocol_qos(); + self + } + + pub(crate) fn locality(mut self, locality: zenoh::sample::Locality) -> Self { + self.locality = Some(locality); + self + } + + pub(crate) fn transient_local_replay_timeout(mut self, timeout: Duration) -> Self { + self.transient_local_replay_timeout = timeout; + self + } +} + +pub struct SubscriberBuilder::Codec> { + pub(crate) context: EndpointBuilderContext, + pub(crate) topic: String, + pub(crate) type_source: MessageEndpointType, + pub(crate) options: SubscriberOptions, pub(crate) _phantom_data: PhantomData<(T, C)>, } @@ -43,6 +76,13 @@ pub(super) struct SubscriberResources { _liveliness_token: LivelinessToken, } +struct PreparedSubscriberBuild { + context: EndpointBuilderContext, + options: SubscriberOptions, + dyn_schema: Option, + entity: EndpointEntity, +} + #[derive(Debug, Clone)] struct QueueDropContext { log_prefix: &'static str, @@ -104,12 +144,28 @@ async fn declare_liveliness(session: &Session, entity: &EndpointEntity) -> Resul .map_err(|source| crate::Error::zenoh("declare subscriber liveliness token", source)) } -impl SubscriberBuilder -where - T: Send + Sync + 'static, -{ +impl SubscriberBuilder { + pub(crate) fn new( + context: EndpointBuilderContext, + topic: String, + type_source: MessageEndpointType, + ) -> Self { + Self { + context, + topic, + type_source, + options: SubscriberOptions::default(), + _phantom_data: Default::default(), + } + } + + pub(crate) fn options(mut self, options: SubscriberOptions) -> Self { + self.options = options; + self + } + pub fn qos(mut self, qos: QosProfile) -> Self { - self.entity.qos = qos.to_protocol_qos(); + self.options = self.options.qos(qos); self } @@ -124,24 +180,18 @@ where /// use zenoh::sample::Locality; /// /// let subscriber = node - /// .subscriber::("/topic")? + /// .subscriber::("/topic") /// .locality(Locality::Remote) // Only receive from remote publishers /// .build() /// .await?; /// ``` pub fn locality(mut self, locality: zenoh::sample::Locality) -> Self { - self.locality = Some(locality); + self.options = self.options.locality(locality); self } pub fn transient_local_replay_timeout(mut self, timeout: Duration) -> Self { - self.transient_local_replay_timeout = timeout; - self - } - - /// Set the dynamic schema for runtime-typed messages. - pub fn dynamic_schema(mut self, schema: Schema) -> Self { - self.dyn_schema = Some(schema); + self.options = self.options.transient_local_replay_timeout(timeout); self } @@ -152,15 +202,46 @@ where RawSubscriberBuilder { inner: self } } - pub(crate) async fn build_raw_queue_async(mut self) -> Result { - let queue_size = subscriber_queue_capacity(&self.entity.qos); + fn prepare_build(self, log_prefix: &str) -> Result { + let Self { + context, + topic, + type_source, + options, + .. + } = self; + let (type_info, dyn_schema) = type_source.resolve_for_subscriber(&topic)?; + let qualified_topic = + qualify_topic_name(&topic, &context.node.namespace, &context.node.name) + .map_err(|source| crate::Error::topic_name(topic, source))?; + + let entity = context.endpoint_entity( + EndpointKind::Subscription, + qualified_topic, + type_info, + options.qos, + ); + debug!("[{}] Qualified topic: {}", log_prefix, entity.topic); + Ok(PreparedSubscriberBuild { + context, + options, + dyn_schema, + entity, + }) + } + + pub(crate) async fn build_raw_queue_async(self) -> Result { + let prepared = self.prepare_build("RAW_SUB")?; + let entity = &prepared.entity; + let queue_size = subscriber_queue_capacity(&entity.qos); let queue = Arc::new(BoundedQueue::new(queue_size)); let raw_queue = queue.clone(); let dropped_samples = Arc::new(AtomicU64::new(0)); let raw_dropped_samples = dropped_samples.clone(); - let drop_context = QueueDropContext::from_entity("RAW_SUB", &self.entity, queue_size)?; - let resources = self + let drop_context = QueueDropContext::from_entity("RAW_SUB", entity, queue_size)?; + let resources = prepared .build_subscriber_resources( + entity, move |sample| { record_queue_push(&raw_queue, &raw_dropped_samples, &drop_context, sample); }, @@ -168,13 +249,19 @@ where ) .await?; - self.warn_about_incompatible_endpoints("RAW_SUB"); + prepared.warn_about_incompatible_endpoints("RAW_SUB"); Ok(raw::RawSubscriber::new(queue, resources)) } +} +impl PreparedSubscriberBuild { fn warn_about_incompatible_endpoints(&self, log_prefix: &str) { - for endpoint in self.graph.type_incompatible_endpoints_for(&self.entity) { + for endpoint in self + .context + .graph + .type_incompatible_endpoints_for(&self.entity) + { warn!( topic = %self.entity.topic, subscriber_node = %self.entity.node.fully_qualified_name(), @@ -190,53 +277,46 @@ where } async fn build_subscriber_resources( - &mut self, + &self, + entity: &EndpointEntity, callback: F, log_prefix: &str, ) -> Result where F: Fn(Sample) + Send + Sync + 'static, { - let topic = self.entity.topic.clone(); - let qualified_topic = - qualify_topic_name(&topic, &self.entity.node.namespace, &self.entity.node.name) - .map_err(|source| crate::Error::topic_name(topic, source))?; - - self.entity.topic = qualified_topic.clone(); - debug!("[{}] Qualified topic: {}", log_prefix, qualified_topic); - - let topic_key_expr = ros_z_protocol::format::topic_key_expr(&self.entity)?; + let topic_key_expr = ros_z_protocol::format::topic_key_expr(entity)?; let key_expr = (*topic_key_expr).clone(); debug!( "[{}] Key expression: {}, qos={:?}", - log_prefix, key_expr, self.entity.qos + log_prefix, key_expr, entity.qos ); let callback: Arc = Arc::new(callback); - if !matches!(self.entity.qos.durability, QosDurability::TransientLocal) { + if !matches!(entity.qos.durability, QosDurability::TransientLocal) { let subscriber_callback = callback.clone(); let mut subscriber = self + .context .session .declare_subscriber(key_expr) .callback(move |sample| subscriber_callback(sample)); - if let Some(locality) = self.locality { + if let Some(locality) = self.options.locality { subscriber = subscriber.allowed_origin(locality); } let subscriber = subscriber .await .map_err(|source| crate::Error::zenoh("declare subscriber", source))?; - let liveliness_token = declare_liveliness(&self.session, &self.entity).await?; + let liveliness_token = declare_liveliness(&self.context.session, entity).await?; Ok(SubscriberResources { _subscriber: subscriber, _liveliness_token: liveliness_token, _replay_guard: None, }) } else { - let Some(live_capacity) = - replay::transient_local_replay_live_capacity(&self.entity.qos) + let Some(live_capacity) = replay::transient_local_replay_live_capacity(&entity.qos) else { warn!( "[{}] TransientLocal + KeepAll requested; replay coordination is disabled because history is unbounded", @@ -244,18 +324,19 @@ where ); let subscriber_callback = callback.clone(); let mut subscriber = self + .context .session .declare_subscriber(key_expr) .callback(move |sample| subscriber_callback(sample)); - if let Some(locality) = self.locality { + if let Some(locality) = self.options.locality { subscriber = subscriber.allowed_origin(locality); } let subscriber = subscriber .await .map_err(|source| crate::Error::zenoh("declare subscriber", source))?; - let liveliness_token = declare_liveliness(&self.session, &self.entity).await?; + let liveliness_token = declare_liveliness(&self.context.session, entity).await?; return Ok(SubscriberResources { _subscriber: subscriber, _liveliness_token: liveliness_token, @@ -270,11 +351,12 @@ where )); let live_coordinator = coordinator.clone(); let mut subscriber = self + .context .session .declare_subscriber(key_expr) .callback(move |sample| live_coordinator.handle_live(sample)); - if let Some(locality) = self.locality { + if let Some(locality) = self.options.locality { subscriber = subscriber.allowed_origin(locality); } @@ -283,53 +365,58 @@ where .map_err(|source| crate::Error::zenoh("declare subscriber", source))?; let (initial_replay_publishers, initial_replay_seen) = replay::initial_replay_plan( - replay::replay_capable_publishers(&self.graph, &self.entity.topic), + replay::replay_capable_publishers(&self.context.graph, &entity.topic), ); for (publisher_global_id, _) in initial_replay_publishers { replay::query_initial_transient_local_replay_async( - &self.session, + &self.context.session, &topic_key_expr, publisher_global_id, - self.transient_local_replay_timeout, + self.options.transient_local_replay_timeout, coordinator.clone(), ) .await?; } coordinator.finish_initial_replay(); let replay_task = replay::spawn_transient_local_replay_task( - self.graph.clone(), - self.entity.topic.clone(), + self.context.graph.clone(), + entity.topic.clone(), coordinator, - self.session.clone(), + self.context.session.clone(), topic_key_expr.to_string(), - self.transient_local_replay_timeout, + self.options.transient_local_replay_timeout, initial_replay_seen, ); - let liveliness_token = declare_liveliness(&self.session, &self.entity).await?; + let replay_guard = replay::TransientLocalReplayGuard::new(cancelled, replay_task); + let liveliness_token = declare_liveliness(&self.context.session, entity).await?; Ok(SubscriberResources { _subscriber: subscriber, _liveliness_token: liveliness_token, - _replay_guard: Some(replay::TransientLocalReplayGuard::new( - cancelled, - replay_task, - )), + _replay_guard: Some(replay_guard), }) } } +} +impl SubscriberBuilder +where + C: WireDecoder, +{ pub async fn build(self) -> Result> where C: WireDecoder, { - let mut builder = self; - let queue_size = subscriber_queue_capacity(&builder.entity.qos); + let prepared = self.prepare_build("SUB")?; + let entity = &prepared.entity; + let queue_size = subscriber_queue_capacity(&entity.qos); let queue = Arc::new(BoundedQueue::new(queue_size)); let subscriber_queue = queue.clone(); let dropped_samples = Arc::new(AtomicU64::new(0)); let subscriber_dropped_samples = dropped_samples.clone(); - let drop_context = QueueDropContext::from_entity("SUB", &builder.entity, queue_size)?; - let resources = builder + let drop_context = QueueDropContext::from_entity("SUB", entity, queue_size)?; + let resources = prepared .build_subscriber_resources( + entity, move |sample| { record_queue_push( &subscriber_queue, @@ -342,18 +429,23 @@ where ) .await?; - builder.warn_about_incompatible_endpoints("SUB"); - - let entity = builder.entity; + prepared.warn_about_incompatible_endpoints("SUB"); debug!("[SUB] Subscriber ready: topic={}", entity.topic); + let PreparedSubscriberBuild { + context, + dyn_schema, + entity, + .. + } = prepared; + Ok(Subscriber { entity, _resources: resources, queue, - graph: builder.graph, - dyn_schema: builder.dyn_schema, + graph: context.graph, + dyn_schema, _phantom_data: Default::default(), }) } @@ -466,14 +558,14 @@ where impl Subscriber { /// Receive and deserialize the next dynamic message. /// - /// This method requires that the subscriber was built through - /// `Node::dynamic_subscriber` or with `.dynamic_schema()`. + /// This method requires that the subscriber was built through a dynamic + /// subscriber factory. /// /// # Errors /// /// Returns an error if: /// - The subscriber was built with a callback (no queue available) - /// - The dynamic schema was not set via `Node::dynamic_subscriber` or `.dynamic_schema()` + /// - The dynamic schema was not set by the dynamic subscriber factory /// - Deserialization fails #[tracing::instrument(name = "recv_dynamic", skip(self), fields( topic = %self.entity.topic, diff --git a/crates/ros-z/src/service.rs b/crates/ros-z/src/service.rs index fddd650e2c..af4b9a6438 100644 --- a/crates/ros-z/src/service.rs +++ b/crates/ros-z/src/service.rs @@ -6,8 +6,7 @@ use std::{ use tracing::{debug, info, trace, warn}; use zenoh::{ - Session, Wait, bytes, key_expr::KeyExpr, liveliness::LivelinessToken, query::Query, - sample::Sample, + Wait, bytes, key_expr::KeyExpr, liveliness::LivelinessToken, query::Query, sample::Sample, }; use std::sync::atomic::Ordering; @@ -16,8 +15,8 @@ use crate::{Error, Result, error::WireError, topic_name}; use crate::{ attachment::{Attachment, EndpointGlobalId}, - entity::EndpointEntity, - graph::Graph, + endpoint_builder::{EndpointBuilderContext, ServiceEndpointType}, + entity::{EndpointEntity, EndpointKind}, message::{Message, Service, WireDecoder, WireEncoder}, qos::QosProfile, queue::BoundedQueue, @@ -26,17 +25,17 @@ use crate::{ #[derive(Debug)] pub struct ServiceClientBuilder { - pub(crate) entity: EndpointEntity, - pub(crate) session: Session, - pub(crate) clock: Clock, - pub(crate) graph: Arc, + pub(crate) context: EndpointBuilderContext, + pub(crate) name: String, + pub(crate) type_source: ServiceEndpointType, + pub(crate) qos: ros_z_protocol::qos::QosProfile, pub(crate) _phantom_data: PhantomData, } /// A native ros-z reusable service handle for typed request/response calls. /// /// Create a client via -/// [`Node::create_service_client`](crate::node::Node::create_service_client). +/// [`Node::service_client`](crate::node::Node::service_client). /// Invoke the service with [`call`](ServiceClient::call) for blocking code or /// [`call_async`](ServiceClient::call_async) for async code. /// @@ -76,34 +75,25 @@ where T: Service, { #[tracing::instrument(name = "client_build", skip(self), fields( - service = %self.entity.topic + service = %self.name ))] - pub async fn build(mut self) -> Result> { - // Qualify the service name as a ros-z graph name. - let topic = self.entity.topic.clone(); - let qualified_service = topic_name::qualify_service_name( - &topic, - &self.entity.node.namespace, - &self.entity.node.name, - ) - .map_err(|source| crate::Error::service_name(topic, source))?; - - self.entity.topic = qualified_service.clone(); - debug!("[CLN] Qualified service: {}", qualified_service); - - let topic_key_expr = ros_z_protocol::format::topic_key_expr(&self.entity)?; + pub async fn build(self) -> Result> { + let entity = self.prepare_entity()?; + let topic_key_expr = ros_z_protocol::format::topic_key_expr(&entity)?; let key_expr = (*topic_key_expr).clone(); debug!("[CLN] Key expression: {}", key_expr); let inner = self + .context .session .declare_querier(key_expr) .target(zenoh::query::QueryTarget::AllComplete) .consolidation(zenoh::query::ConsolidationMode::None) .await .map_err(|source| crate::Error::zenoh("declare service querier", source))?; - let liveliness_key_expr = self.entity.liveliness_key_expr()?.0; + let liveliness_key_expr = entity.liveliness_key_expr()?.0; let lv_token = self + .context .session .liveliness() .declare_token(liveliness_key_expr) @@ -111,29 +101,29 @@ where .map_err(|source| { crate::Error::zenoh("declare service client liveliness token", source) })?; - self.warn_about_incompatible_endpoints(); - debug!("[CLN] Client ready: service={}", self.entity.topic); + self.warn_about_incompatible_endpoints(&entity); + debug!("[CLN] Client ready: service={}", entity.topic); Ok(ServiceClient { sequence_number: AtomicUsize::new(1), // Start at 1; zero is reserved for missing sequence values. inner, _lv_token: lv_token, - endpoint_global_id: EndpointGlobalId::from(&self.entity), - topic: self.entity.topic, - clock: self.clock, + endpoint_global_id: EndpointGlobalId::from(&entity), + topic: entity.topic, + clock: self.context.clock, _phantom_data: Default::default(), }) } - fn warn_about_incompatible_endpoints(&self) -> usize { - let endpoints = self.graph.type_incompatible_endpoints_for(&self.entity); + fn warn_about_incompatible_endpoints(&self, entity: &EndpointEntity) -> usize { + let endpoints = self.context.graph.type_incompatible_endpoints_for(entity); let count = endpoints.len(); for endpoint in endpoints { warn!( - service = %self.entity.topic, - client_node = %self.entity.node.fully_qualified_name(), - client_type = %self.entity.type_info.name, - client_schema_hash = %self.entity.type_info.hash, + service = %entity.topic, + client_node = %entity.node.fully_qualified_name(), + client_type = %entity.type_info.name, + client_schema_hash = %entity.type_info.hash, endpoint_kind = ?endpoint.kind, endpoint_node = %endpoint.node.fully_qualified_name(), endpoint_type = %endpoint.type_info.name, @@ -360,36 +350,94 @@ where #[derive(Debug)] pub struct ServiceServerBuilder { - pub(crate) entity: EndpointEntity, - pub(crate) session: Session, - pub(crate) clock: Clock, - pub(crate) graph: Arc, + pub(crate) context: EndpointBuilderContext, + pub(crate) name: String, + pub(crate) type_source: ServiceEndpointType, + pub(crate) qos: ros_z_protocol::qos::QosProfile, pub(crate) _phantom_data: PhantomData, } +fn prepare_service_endpoint( + context: &EndpointBuilderContext, + name: &str, + type_source: &ServiceEndpointType, + kind: EndpointKind, + qos: ros_z_protocol::qos::QosProfile, + log_prefix: &str, +) -> Result { + let type_info = type_source.resolve(); + let qualified_service = + topic_name::qualify_service_name(name, &context.node.namespace, &context.node.name) + .map_err(|source| crate::Error::service_name(name, source))?; + + debug!("[{}] Qualified service: {}", log_prefix, qualified_service); + + Ok(context.endpoint_entity(kind, qualified_service, type_info, qos)) +} + impl ServiceClientBuilder { - /// Set the QoS profile for this client. - pub fn with_qos(mut self, qos: QosProfile) -> Self { - self.entity.qos = qos.to_protocol_qos(); - self + pub(crate) fn new( + context: crate::endpoint_builder::EndpointBuilderContext, + name: String, + type_source: crate::endpoint_builder::ServiceEndpointType, + ) -> Self { + Self { + context, + name, + type_source, + qos: crate::endpoint_builder::default_protocol_qos(), + _phantom_data: Default::default(), + } } - /// Get a reference to the native ros-z entity. - pub fn entity(&self) -> &EndpointEntity { - &self.entity + fn prepare_entity(&self) -> Result { + prepare_service_endpoint( + &self.context, + &self.name, + &self.type_source, + EndpointKind::Client, + self.qos, + "CLN", + ) + } + + /// Set the QoS profile for this client. + pub fn qos(mut self, qos: QosProfile) -> Self { + self.qos = qos.to_protocol_qos(); + self } } impl ServiceServerBuilder { - /// Set the QoS profile for this server. - pub fn with_qos(mut self, qos: QosProfile) -> Self { - self.entity.qos = qos.to_protocol_qos(); - self + pub(crate) fn new( + context: crate::endpoint_builder::EndpointBuilderContext, + name: String, + type_source: crate::endpoint_builder::ServiceEndpointType, + ) -> Self { + Self { + context, + name, + type_source, + qos: crate::endpoint_builder::default_protocol_qos(), + _phantom_data: Default::default(), + } } - /// Get a reference to the native ros-z entity. - pub fn entity(&self) -> &EndpointEntity { - &self.entity + fn prepare_entity(&self) -> Result { + prepare_service_endpoint( + &self.context, + &self.name, + &self.type_source, + EndpointKind::Service, + self.qos, + "SRV", + ) + } + + /// Set the QoS profile for this server. + pub fn qos(mut self, qos: QosProfile) -> Self { + self.qos = qos.to_protocol_qos(); + self } } @@ -456,27 +504,19 @@ where { /// Internal method that all build variants use. async fn build_internal( - mut self, + self, handler: ServiceQueryHandler, queue: Option>>, ) -> Result> { - let topic = self.entity.topic.clone(); - let qualified_service = topic_name::qualify_service_name( - &topic, - &self.entity.node.namespace, - &self.entity.node.name, - ) - .map_err(|source| crate::Error::service_name(topic, source))?; - - self.entity.topic = qualified_service.clone(); - - let topic_key_expr = ros_z_protocol::format::topic_key_expr(&self.entity)?; + let entity = self.prepare_entity()?; + let topic_key_expr = ros_z_protocol::format::topic_key_expr(&entity)?; let key_expr = (*topic_key_expr).clone(); tracing::debug!("[SRV] KE: {key_expr}"); info!("[SRV] Declaring queryable on key expression: {}", key_expr); let inner = self + .context .session .declare_queryable(&key_expr) .complete(true) @@ -505,8 +545,9 @@ where .await .map_err(|source| crate::Error::zenoh("declare service queryable", source))?; - let liveliness_key_expr = self.entity.liveliness_key_expr()?.0; + let liveliness_key_expr = entity.liveliness_key_expr()?.0; let lv_token = self + .context .session .liveliness() .declare_token(liveliness_key_expr) @@ -514,13 +555,13 @@ where .map_err(|source| { crate::Error::zenoh("declare service server liveliness token", source) })?; - self.warn_about_incompatible_endpoints(); + self.warn_about_incompatible_endpoints(&entity); Ok(ServiceServer { key_expr, _inner: inner, _lv_token: lv_token, - clock: self.clock, + clock: self.context.clock, queue, _phantom_data: Default::default(), }) @@ -535,7 +576,7 @@ where } pub async fn build(self) -> Result> { - let queue_size = match self.entity.qos.history { + let queue_size = match self.qos.history { ros_z_protocol::qos::QosHistory::KeepLast(depth) => depth, ros_z_protocol::qos::QosHistory::KeepAll => usize::MAX, }; @@ -544,15 +585,15 @@ where .await } - fn warn_about_incompatible_endpoints(&self) -> usize { - let endpoints = self.graph.type_incompatible_endpoints_for(&self.entity); + fn warn_about_incompatible_endpoints(&self, entity: &EndpointEntity) -> usize { + let endpoints = self.context.graph.type_incompatible_endpoints_for(entity); let count = endpoints.len(); for endpoint in endpoints { warn!( - service = %self.entity.topic, - server_node = %self.entity.node.fully_qualified_name(), - server_type = %self.entity.type_info.name, - server_schema_hash = %self.entity.type_info.hash, + service = %entity.topic, + server_node = %entity.node.fully_qualified_name(), + server_type = %entity.type_info.name, + server_schema_hash = %entity.type_info.hash, endpoint_kind = ?endpoint.kind, endpoint_node = %endpoint.node.fully_qualified_name(), endpoint_type = %endpoint.type_info.name, @@ -889,24 +930,34 @@ mod tests { .await .expect("Failed to create node"); - let server_builder = node - .create_service_server::("/service_warning_helpers") - .expect("service server factory should succeed"); - let client_builder = node - .create_service_client::("/service_warning_helpers") - .expect("service client factory should succeed"); + let server_builder = node.service_server::("/service_warning_helpers"); + let client_builder = node.service_client::("/service_warning_helpers"); + let server_entity = server_builder + .prepare_entity() + .expect("service server entity should prepare"); + let client_entity = client_builder + .prepare_entity() + .expect("service client entity should prepare"); server_builder + .context .graph - .add_local_entity(Entity::Endpoint(client_builder.entity.clone())) + .add_local_entity(Entity::Endpoint(client_entity.clone())) .expect("client endpoint should be inserted"); client_builder + .context .graph - .add_local_entity(Entity::Endpoint(server_builder.entity.clone())) + .add_local_entity(Entity::Endpoint(server_entity.clone())) .expect("server endpoint should be inserted"); - assert_eq!(server_builder.warn_about_incompatible_endpoints(), 1); - assert_eq!(client_builder.warn_about_incompatible_endpoints(), 1); + assert_eq!( + server_builder.warn_about_incompatible_endpoints(&server_entity), + 1 + ); + assert_eq!( + client_builder.warn_about_incompatible_endpoints(&client_entity), + 1 + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -930,8 +981,7 @@ mod tests { .expect("Failed to create client node"); let mut server = server_node - .create_service_server::("raw_query_add_two_ints") - .expect("service server factory should succeed") + .service_server::("raw_query_add_two_ints") .build() .await .expect("Failed to create service server"); diff --git a/crates/ros-z/src/shm.rs b/crates/ros-z/src/shm.rs index 414079dd95..ae54c66e78 100644 --- a/crates/ros-z/src/shm.rs +++ b/crates/ros-z/src/shm.rs @@ -52,7 +52,7 @@ //! let provider = Arc::new(ShmProviderBuilder::new(20_000_000).build()?); //! let config = ShmConfig::new(provider).with_threshold(10_000); //! -//! let publisher = node.publisher::("topic")? +//! let publisher = node.publisher::("topic") //! .shm_config(config) //! .build() //! .await?; diff --git a/crates/ros-z/tests/communication_pubsub_alignment.rs b/crates/ros-z/tests/communication_pubsub_alignment.rs index 027aad9c5c..8dd695cf23 100644 --- a/crates/ros-z/tests/communication_pubsub_alignment.rs +++ b/crates/ros-z/tests/communication_pubsub_alignment.rs @@ -23,13 +23,11 @@ async fn publisher_message_id_is_reserved_and_observable() { let publisher = node .publisher::("/alignment_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher"); let subscriber = node .subscriber::("/alignment_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -73,13 +71,11 @@ async fn publisher_direct_publish_attaches_publication_id() { let publisher = node .publisher::("/alignment_direct_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher"); let subscriber = node .subscriber::("/alignment_direct_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); diff --git a/crates/ros-z/tests/endpoint_builder_api.rs b/crates/ros-z/tests/endpoint_builder_api.rs new file mode 100644 index 0000000000..8440fc52ec --- /dev/null +++ b/crates/ros-z/tests/endpoint_builder_api.rs @@ -0,0 +1,6 @@ +#[test] +fn endpoint_builders_expose_single_question_mark_api() { + let cases = trybuild::TestCases::new(); + cases.pass("tests/ui/endpoint_builder/new_api_pass.rs"); + cases.compile_fail("tests/ui/endpoint_builder/dynamic_subscriber_cache.rs"); +} diff --git a/crates/ros-z/tests/extended_type_info.rs b/crates/ros-z/tests/extended_type_info.rs index 8ff1b8dffe..74e08966f3 100644 --- a/crates/ros-z/tests/extended_type_info.rs +++ b/crates/ros-z/tests/extended_type_info.rs @@ -302,7 +302,6 @@ async fn discovery_uses_schema_service_for_standard_compatible_types() { let publisher = pub_node .publisher::("/extended_standard_topic") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -342,11 +341,9 @@ async fn discovery_uses_schema_service_for_standard_compatible_types() { let subscriber = sub_node .dynamic_subscriber_auto("/extended_standard_topic", Duration::from_secs(10)) - .await - .expect("dynamic subscriber") .build() .await - .expect("subscriber build"); + .expect("dynamic subscriber build"); let schema = subscriber.schema().expect("discovered schema"); assert_eq!( @@ -379,7 +376,6 @@ async fn schema_service_round_trips_recursive_bundle() { let publisher = pub_node .publisher::("/recursive_trace_topic") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -424,11 +420,9 @@ async fn schema_service_round_trips_recursive_bundle() { let subscriber = sub_node .dynamic_subscriber_auto("/recursive_trace_topic", Duration::from_secs(10)) - .await - .expect("dynamic subscriber") .build() .await - .expect("subscriber build"); + .expect("dynamic subscriber build"); let schema = subscriber.schema().expect("discovered schema"); assert_eq!(schema_type_name(schema), RecursiveTrace::type_name()); assert!(schema_has_recursive_children(schema)); @@ -465,7 +459,6 @@ async fn extended_discovery_should_fail_when_the_publisher_disabled_the_schema_s let publisher = pub_node .publisher::("/extended_robot_topic") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -497,6 +490,7 @@ async fn extended_discovery_should_fail_when_the_publisher_disabled_the_schema_s let result = sub_node .dynamic_subscriber_auto("/extended_robot_topic", Duration::from_secs(3)) + .build() .await; assert!( result.is_err(), @@ -521,7 +515,6 @@ async fn extended_only_types_use_schema_service_when_enabled() { let publisher = pub_node .publisher::("/extended_robot_topic") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -562,11 +555,9 @@ async fn extended_only_types_use_schema_service_when_enabled() { let subscriber = sub_node .dynamic_subscriber_auto("/extended_robot_topic", Duration::from_secs(10)) - .await - .expect("dynamic subscriber") .build() .await - .expect("subscriber build"); + .expect("dynamic subscriber build"); let schema = subscriber.schema().expect("discovered schema"); assert!(uses_extended_types(schema)); @@ -622,7 +613,6 @@ async fn type_description_discovery_works_across_namespaces_for_extended_types() let publisher = pub_node .publisher::("/extended_robot_topic") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -655,11 +645,9 @@ async fn type_description_discovery_works_across_namespaces_for_extended_types() let subscriber = sub_node .dynamic_subscriber_auto("/extended_robot_topic", Duration::from_secs(10)) - .await - .expect("dynamic subscriber") .build() .await - .expect("subscriber build"); + .expect("dynamic subscriber build"); let schema = subscriber.schema().expect("discovered schema"); assert!(uses_extended_types(schema)); @@ -692,7 +680,6 @@ async fn top_level_enums_are_discoverable_through_the_schema_service() { let publisher = pub_node .publisher::("/robot_state_topic") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -720,11 +707,9 @@ async fn top_level_enums_are_discoverable_through_the_schema_service() { let subscriber = sub_node .dynamic_subscriber_auto("/robot_state_topic", Duration::from_secs(10)) - .await - .expect("enum discovery") .build() .await - .expect("subscriber build"); + .expect("enum discovery and subscriber build"); let schema = subscriber.schema().expect("discovered schema"); let variants = shape_variants(schema); diff --git a/crates/ros-z/tests/graph.rs b/crates/ros-z/tests/graph.rs index 740421a8a7..8392d20c12 100644 --- a/crates/ros-z/tests/graph.rs +++ b/crates/ros-z/tests/graph.rs @@ -555,12 +555,12 @@ mod tests { }; let _publisher = pub_node - .publisher::(&topic)? + .publisher::(&topic) .qos(pub_qos) .build() .await?; let _subscriber = sub_node - .subscriber::(&topic)? + .subscriber::(&topic) .qos(sub_qos) .build() .await?; @@ -602,12 +602,12 @@ mod tests { }; let _publisher = pub_node - .publisher::(&topic)? + .publisher::(&topic) .qos(pub_qos) .build() .await?; let _subscriber = sub_node - .subscriber::(&topic)? + .subscriber::(&topic) .qos(sub_qos) .build() .await?; @@ -630,7 +630,7 @@ mod tests { let (_ctx, node) = setup_test_node("test_graph_node").await?; let topic_name = unique_graph_name("test_graph_topic_names_and_types"); - let _pub = node.publisher::(&topic_name)?.build().await?; + let _pub = node.publisher::(&topic_name).build().await?; assert!( wait_for_publishers(&node, &topic_name, 1, 1_000).await?, "Expected graph to discover publisher for {topic_name}" @@ -653,7 +653,7 @@ mod tests { let service_name = unique_graph_name("test_graph_service_names_and_types"); let _service = node - .create_service_server::(&service_name)? + .service_server::(&service_name) .build() .await?; assert!( @@ -682,7 +682,7 @@ mod tests { assert_eq!(count, 0, "Expected 0 publishers on non-existent topic"); - let _pub = node.publisher::(&topic_name)?.build().await?; + let _pub = node.publisher::(&topic_name).build().await?; assert!(wait_for_publishers(&node, &topic_name, 1, 1_000).await?); @@ -704,7 +704,7 @@ mod tests { let count = graph.view().subscriptions_on(&topic_name).len(); assert_eq!(count, 0, "Expected 0 subscribers on non-existent topic"); - let _sub = node.subscriber::(&topic_name)?.build().await?; + let _sub = node.subscriber::(&topic_name).build().await?; assert!(wait_for_subscribers(&node, &topic_name, 1, 1_000).await?); @@ -728,7 +728,7 @@ mod tests { assert_eq!(count, 0, "Expected 0 clients on non-existent service"); let _client = node - .create_service_client::(&service_name)? + .service_client::(&service_name) .build() .await?; @@ -751,7 +751,7 @@ mod tests { assert_eq!(count, 0, "Expected 0 services on non-existent service"); let _service = node - .create_service_server::(&service_name)? + .service_server::(&service_name) .build() .await?; @@ -769,7 +769,7 @@ mod tests { let service_name = unique_graph_name("graph_service_by_node"); let _service = node - .create_service_server::(&service_name)? + .service_server::(&service_name) .build() .await?; @@ -800,7 +800,7 @@ mod tests { let service_name = unique_graph_name("graph_client_by_node"); let _client = node - .create_service_client::(&service_name)? + .service_client::(&service_name) .build() .await?; @@ -843,7 +843,7 @@ mod tests { assert_eq!(count_pubs, 0, "Expected 0 publishers initially"); assert_eq!(count_subs, 0, "Expected 0 subscribers initially"); - let pub_handle = node.publisher::(&topic_name)?.build().await?; + let pub_handle = node.publisher::(&topic_name).build().await?; assert!(wait_for_publishers(&node, &topic_name, 1, 1_000).await?); @@ -853,7 +853,7 @@ mod tests { "Expected at least 1 publisher after creation" ); - let sub_handle = node.subscriber::(&topic_name)?.build().await?; + let sub_handle = node.subscriber::(&topic_name).build().await?; assert!(wait_for_subscribers(&node, &topic_name, 1, 1_000).await?); @@ -911,8 +911,8 @@ mod tests { let topic_name = unique_graph_name("test_multi_node_pub"); - let _pub1 = node1.publisher::(&topic_name)?.build().await?; - let _pub2 = node2.publisher::(&topic_name)?.build().await?; + let _pub1 = node1.publisher::(&topic_name).build().await?; + let _pub2 = node2.publisher::(&topic_name).build().await?; assert!( wait_for_publishers(&node1, &topic_name, 2, 1_000).await?, @@ -940,8 +940,8 @@ mod tests { let topic_name = unique_graph_name("test_multi_node_sub"); - let _sub1 = node1.subscriber::(&topic_name)?.build().await?; - let _sub2 = node2.subscriber::(&topic_name)?.build().await?; + let _sub1 = node1.subscriber::(&topic_name).build().await?; + let _sub2 = node2.subscriber::(&topic_name).build().await?; assert!( wait_for_subscribers(&node1, &topic_name, 2, 1_000).await?, @@ -971,11 +971,11 @@ mod tests { let service_name2 = unique_graph_name("graph_multi_node_service_2"); let _srv1 = node1 - .create_service_server::(&service_name1)? + .service_server::(&service_name1) .build() .await?; let _srv2 = node2 - .create_service_server::(&service_name2)? + .service_server::(&service_name2) .build() .await?; @@ -1012,15 +1012,15 @@ mod tests { let service_name = unique_graph_name("graph_multi_node_client"); let _srv = node1 - .create_service_server::(&service_name)? + .service_server::(&service_name) .build() .await?; let _client1 = node1 - .create_service_client::(&service_name)? + .service_client::(&service_name) .build() .await?; let _client2 = node2 - .create_service_client::(&service_name)? + .service_client::(&service_name) .build() .await?; @@ -1039,7 +1039,7 @@ mod tests { let service_name = unique_graph_name("graph_service_available"); let client = node - .create_service_client::(&service_name)? + .service_client::(&service_name) .build() .await?; @@ -1048,7 +1048,7 @@ mod tests { assert_eq!(count, 0, "Expected 0 services before creating server"); let service = node - .create_service_server::(&service_name)? + .service_server::(&service_name) .build() .await?; @@ -1072,8 +1072,8 @@ mod tests { let (_ctx, node) = setup_test_node("test_graph_node").await?; let topic_name = unique_graph_name("graph_entities_by_topic"); - let _pub = node.publisher::(&topic_name)?.build().await?; - let _sub = node.subscriber::(&topic_name)?.build().await?; + let _pub = node.publisher::(&topic_name).build().await?; + let _sub = node.subscriber::(&topic_name).build().await?; assert!(wait_for_publishers(&node, &topic_name, 1, 1_000).await?); assert!(wait_for_subscribers(&node, &topic_name, 1, 1_000).await?); diff --git a/crates/ros-z/tests/nalgebra_field_type_info.rs b/crates/ros-z/tests/nalgebra_field_type_info.rs index bda663a61a..9cdeba827c 100644 --- a/crates/ros-z/tests/nalgebra_field_type_info.rs +++ b/crates/ros-z/tests/nalgebra_field_type_info.rs @@ -312,7 +312,6 @@ async fn nalgebra_fields_roundtrip_via_standard_discovery() { let publisher = pub_node .publisher::("/math_snapshot") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -337,11 +336,9 @@ async fn nalgebra_fields_roundtrip_via_standard_discovery() { wait_for_publishers(&sub_node, "/math_snapshot", 1, Duration::from_secs(2)).await; let subscriber = sub_node .dynamic_subscriber_auto("/math_snapshot", Duration::from_secs(10)) - .await - .expect("subscriber discovery") .build() .await - .expect("subscriber build"); + .expect("subscriber discovery and build"); let schema = subscriber.schema().expect("discovered schema"); assert!(!schema_uses_extended_types(schema)); @@ -385,7 +382,6 @@ async fn single_schema_discovery_works_with_basic_nalgebra_fields() { let publisher = pub_node .publisher::("/math_command") - .expect("endpoint factory should succeed") .build() .await .expect("publisher"); @@ -415,11 +411,9 @@ async fn single_schema_discovery_works_with_basic_nalgebra_fields() { wait_for_publishers(&sub_node, "/math_command", 1, Duration::from_secs(2)).await; let subscriber = sub_node .dynamic_subscriber_auto("/math_command", Duration::from_secs(10)) - .await - .expect("subscriber discovery") .build() .await - .expect("subscriber build"); + .expect("subscriber discovery and build"); let schema = subscriber.schema().expect("discovered schema"); assert!(schema_uses_extended_types(schema)); diff --git a/crates/ros-z/tests/parameter_integration.rs b/crates/ros-z/tests/parameter_integration.rs index c6fcdd9925..4b3b44c542 100644 --- a/crates/ros-z/tests/parameter_integration.rs +++ b/crates/ros-z/tests/parameter_integration.rs @@ -380,9 +380,9 @@ async fn remote_v1_services_work() -> TestResult { .await?; let snapshot_client = client_node - .create_service_client::( + .service_client::( "/motion/walk_publisher/parameter/get_snapshot", - )? + ) .build() .await?; let snapshot = snapshot_client.call_async(&Default::default()).await?; @@ -391,7 +391,7 @@ async fn remote_v1_services_work() -> TestResult { assert!(snapshot.value_json.contains("threshold")); let set_client = client_node - .create_service_client::("/motion/walk_publisher/parameter/set")? + .service_client::("/motion/walk_publisher/parameter/set") .build() .await?; let set_response = set_client @@ -405,9 +405,7 @@ async fn remote_v1_services_work() -> TestResult { assert!(set_response.success); let value_client = client_node - .create_service_client::( - "/motion/walk_publisher/parameter/get_value", - )? + .service_client::("/motion/walk_publisher/parameter/get_value") .build() .await?; let value_response = value_client @@ -451,9 +449,9 @@ async fn bound_parameters_expose_type_info_and_schema_lookup() -> TestResult { ) .await?; let type_info_client = client_node - .create_service_client::( + .service_client::( "/motion/walk_publisher/parameter/get_type_info", - )? + ) .build() .await?; let type_info = type_info_client.call_async(&Default::default()).await?; diff --git a/crates/ros-z/tests/pubsub.rs b/crates/ros-z/tests/pubsub.rs index dd4bd17f1d..a506e69f1b 100644 --- a/crates/ros-z/tests/pubsub.rs +++ b/crates/ros-z/tests/pubsub.rs @@ -4,7 +4,7 @@ use ros_z::{ Message, attachment::Attachment, context::ContextBuilder, - entity::{EndpointEntity, EndpointKind, TypeInfo}, + entity::{EndpointEntity, EndpointKind, SchemaHash, TypeInfo}, message::{SerdeCdrCodec, WireEncoder}, qos::{QosDurability, QosHistory, QosProfile, QosReliability}, schema::SchemaBuilder, @@ -52,6 +52,17 @@ fn mismatched_dynamic_schema() -> ros_z::dynamic::Schema { }) } +fn dynamic_schema(root_name: &str) -> ros_z::dynamic::Schema { + let root = TypeName::new(root_name).unwrap(); + std::sync::Arc::new(SchemaBundle { + root: TypeDef::Named(root.clone()), + definitions: TypeDefinitions::from([( + root, + TypeDefinition::Struct(StructDef { fields: vec![] }), + )]), + }) +} + async fn test_context() -> ros_z::Result { ContextBuilder::default().build().await } @@ -135,9 +146,9 @@ async fn node_builder_rejects_protocol_reserved_identity_before_formatting() -> async fn raw_subscriber_receives_sample_payload() -> zenoh::Result<()> { let context = ContextBuilder::default().build().await?; let node = context.create_node("raw_subscriber_node").build().await?; - let publisher = node.publisher::("/raw_topic")?.build().await?; + let publisher = node.publisher::("/raw_topic").build().await?; let mut subscriber = node - .subscriber::("/raw_topic")? + .subscriber::("/raw_topic") .raw() .build() .await?; @@ -156,7 +167,7 @@ async fn raw_subscriber_receives_sample_payload() -> zenoh::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn dynamic_publisher_factory_rejects_schema_root_that_differs_from_type_info() { +async fn dynamic_publisher_build_rejects_schema_root_that_differs_from_type_info() { let context = ContextBuilder::default() .build() .await @@ -172,10 +183,12 @@ async fn dynamic_publisher_factory_rejects_schema_root_that_differs_from_type_in ros_z_schema::compute_hash(schema.as_ref()).unwrap(), ); - let Err(error) = node.dynamic_publisher("/mismatched_dynamic_schema_root", type_info, schema) - else { - panic!("mismatched schema root should fail dynamic publisher factory"); - }; + let builder = node.dynamic_publisher("/mismatched_dynamic_schema_root", type_info, schema); + let error = builder + .build() + .await + .expect_err("mismatched schema root should fail dynamic publisher build"); + match error { ros_z::Error::Wire(source) => match source.as_ref() { ros_z::error::WireError::DynamicSchema { @@ -199,6 +212,141 @@ async fn dynamic_publisher_factory_rejects_schema_root_that_differs_from_type_in } } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn dynamic_publisher_build_rejects_schema_hash_that_differs_from_type_info() { + let context = ContextBuilder::default() + .build() + .await + .expect("Failed to create context"); + let node = context + .create_node("mismatched_dynamic_schema_hash_publisher") + .build() + .await + .expect("Failed to create node"); + let schema = dynamic_schema("test_msgs::HashCheckedDynamicRoot"); + let type_info = TypeInfo::new("test_msgs::HashCheckedDynamicRoot", SchemaHash::zero()); + + let builder = node.dynamic_publisher("/mismatched_dynamic_schema_hash", type_info, schema); + let error = builder + .build() + .await + .expect_err("mismatched schema hash should fail dynamic publisher build"); + + match error { + ros_z::Error::Wire(source) => match source.as_ref() { + ros_z::error::WireError::DynamicSchema { + endpoint_kind, + topic, + source, + } => { + assert_eq!(*endpoint_kind, "publisher"); + assert_eq!(topic, "/mismatched_dynamic_schema_hash"); + assert!(source.to_string().contains("schema hash")); + } + other => panic!("expected dynamic schema wire error, got {other:?}"), + }, + other => panic!("expected wire error, got {other:?}"), + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn dynamic_subscriber_build_rejects_schema_hash_that_differs_from_type_info() { + let context = ContextBuilder::default() + .build() + .await + .expect("Failed to create context"); + let node = context + .create_node("mismatched_dynamic_schema_hash_subscriber") + .build() + .await + .expect("Failed to create node"); + let schema = dynamic_schema("test_msgs::HashCheckedDynamicRoot"); + let type_info = TypeInfo::new("test_msgs::HashCheckedDynamicRoot", SchemaHash::zero()); + + let builder = node.dynamic_subscriber("/mismatched_dynamic_schema_hash", type_info, schema); + let error = builder + .build() + .await + .expect_err("mismatched schema hash should fail dynamic subscriber build"); + + match error { + ros_z::Error::Wire(source) => match source.as_ref() { + ros_z::error::WireError::DynamicSchema { + endpoint_kind, + topic, + source, + } => { + assert_eq!(*endpoint_kind, "subscriber"); + assert_eq!(topic, "/mismatched_dynamic_schema_hash"); + assert!(source.to_string().contains("schema hash")); + } + other => panic!("expected dynamic schema wire error, got {other:?}"), + }, + other => panic!("expected wire error, got {other:?}"), + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn endpoint_factories_defer_topic_errors_to_build() { + let context = ContextBuilder::default() + .build() + .await + .expect("Failed to create context"); + let node = context + .create_node("deferred_topic_errors") + .build() + .await + .expect("Failed to create node"); + + let publisher_error = node + .publisher::("bad%topic") + .build() + .await + .expect_err("invalid publisher topic should fail during build"); + assert!(matches!( + publisher_error, + ros_z::Error::Name { + kind: ros_z::error::NameKind::Topic, + .. + } + )); + + let subscriber_error = node + .subscriber::("bad%topic") + .build() + .await + .expect_err("invalid subscriber topic should fail during build"); + assert!(matches!( + subscriber_error, + ros_z::Error::Name { + kind: ros_z::error::NameKind::Topic, + .. + } + )); + + let dynamic_auto_error = node + .dynamic_subscriber_auto("bad%topic", Duration::from_millis(1)) + .build() + .await + .expect_err("invalid dynamic auto-subscriber topic should fail during build"); + assert!(matches!( + dynamic_auto_error, + ros_z::Error::Name { + kind: ros_z::error::NameKind::Topic, + .. + } + )); + + let schema_discovery_error = node + .discover_topic_schema("bad%topic", Duration::from_millis(1)) + .await + .expect_err("invalid schema-discovery topic should fail before graph lookup"); + assert!(matches!( + schema_discovery_error, + ros_z::dynamic::DynamicError::Name { .. } + )); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_basic_pubsub() { let context = ContextBuilder::default() @@ -213,14 +361,12 @@ async fn test_basic_pubsub() { let publisher = node .publisher::("/test_topic") - .expect("publisher factory should succeed") .build() .await .unwrap(); let subscriber = node .subscriber::("/test_topic") - .expect("subscriber factory should succeed") .build() .await .unwrap(); @@ -260,7 +406,7 @@ async fn transient_local_build_waits_for_initial_replay() -> ros_z::Result<()> { }; let publisher = pub_node - .publisher::(topic)? + .publisher::(topic) .qos(qos) .build() .await?; @@ -271,7 +417,7 @@ async fn transient_local_build_waits_for_initial_replay() -> ros_z::Result<()> { publisher.publish(&message).await?; let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -301,14 +447,12 @@ async fn test_multiple_messages() { let publisher = node .publisher::("/multi_topic") - .expect("publisher factory should succeed") .build() .await .unwrap(); let subscriber = node .subscriber::("/multi_topic") - .expect("subscriber factory should succeed") .build() .await .unwrap(); @@ -358,7 +502,6 @@ async fn dynamic_publisher_advertises_explicit_schema_hash() { let _publisher = node .dynamic_publisher(topic, TypeInfo::new(&root_name, schema_hash), root_schema) - .expect("dynamic publisher factory should succeed") .build() .await .expect("publisher should build"); @@ -399,14 +542,12 @@ async fn recv_with_metadata_includes_transport_and_source_timestamps() { let publisher = node .publisher::("/metadata_topic") - .expect("publisher factory should succeed") .build() .await .unwrap(); let subscriber = node .subscriber::("/metadata_topic") - .expect("subscriber factory should succeed") .build() .await .unwrap(); @@ -450,7 +591,6 @@ async fn typed_subscriber_errors_when_sample_has_no_attachment() { let topic = "/missing_attachment_pubsub"; let subscriber = node .subscriber::(topic) - .expect("subscriber factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -497,14 +637,12 @@ async fn test_large_payload() { let publisher = node .publisher::("/large_topic") - .expect("publisher factory should succeed") .build() .await .unwrap(); let subscriber = node .subscriber::("/large_topic") - .expect("subscriber factory should succeed") .build() .await .unwrap(); @@ -544,13 +682,11 @@ async fn test_logical_clock_is_used_for_attachment_timestamps() { let publisher = node .publisher::("/sim_clock") - .expect("publisher factory should succeed") .build() .await .unwrap(); let mut subscriber = node .subscriber::("/sim_clock") - .expect("subscriber factory should succeed") .raw() .build() .await @@ -598,7 +734,6 @@ async fn test_vec_u8_pubsub() { let publisher = node .publisher::>("zbuf_topic") - .expect("publisher factory should succeed") .build() .await .expect("Failed to create publisher"); @@ -629,7 +764,6 @@ async fn test_vec_u8_pubsub() { let subscriber = node .subscriber::>("zbuf_topic") - .expect("subscriber factory should succeed") .build() .await .expect("Failed to create subscriber"); diff --git a/crates/ros-z/tests/pubsub_qos.rs b/crates/ros-z/tests/pubsub_qos.rs index 258849b93b..afacecc90c 100644 --- a/crates/ros-z/tests/pubsub_qos.rs +++ b/crates/ros-z/tests/pubsub_qos.rs @@ -75,17 +75,13 @@ mod tests { ); let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; let receive_task = tokio::spawn(collect_messages(subscriber, 5, Duration::from_secs(5))); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; tokio::time::sleep(Duration::from_millis(2000)).await; @@ -116,14 +112,10 @@ mod tests { QosHistory::KeepLast(NonZeroUsize::new(10).unwrap()), ); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -163,17 +155,13 @@ mod tests { ); let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; let receive_task = tokio::spawn(collect_messages(subscriber, 5, Duration::from_secs(5))); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; tokio::time::sleep(Duration::from_millis(2000)).await; @@ -207,15 +195,11 @@ mod tests { ..Default::default() }; - let publisher = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let publisher = pub_node.publisher::(topic).qos(qos).build().await?; publisher.publish(&"cached".into()).await?; let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -238,17 +222,13 @@ mod tests { ..Default::default() }; - let publisher = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let publisher = pub_node.publisher::(topic).qos(qos).build().await?; for data in ["one", "two", "three"] { publisher.publish(&data.into()).await?; } let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -274,18 +254,14 @@ mod tests { ..Default::default() }; - let publisher = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let publisher = pub_node.publisher::(topic).qos(qos).build().await?; for data in ["cached-1", "cached-2", "cached-3"] { publisher.publish(&data.into()).await?; } let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -324,17 +300,13 @@ mod tests { ..Default::default() }; - let publisher = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let publisher = pub_node.publisher::(topic).qos(qos).build().await?; for data in ["one", "two", "three"] { publisher.publish(&data.into()).await?; } let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -355,7 +327,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn transient_local_replays_last_sample_to_late_cache() -> Result<()> { + async fn transient_local_late_cache_receives_replayed_sample() -> Result<()> { let context = ContextBuilder::default().build().await?; let pub_node = context.create_node("transient_cache_pub").build().await?; let cache_node = context.create_node("transient_cache_late").build().await?; @@ -367,16 +339,13 @@ mod tests { ..Default::default() }; - let publisher = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let publisher = pub_node.publisher::(topic).qos(qos).build().await?; publisher.publish(&"cached".into()).await?; let cache = cache_node - .create_cache::(topic, 1)? - .with_qos(qos) + .subscriber::(topic) + .qos(qos) + .cache(1) .build() .await?; @@ -417,16 +386,12 @@ mod tests { }; let early_subscriber = early_sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .raw() .build() .await?; - let publisher = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let publisher = pub_node.publisher::(topic).qos(qos).build().await?; publisher.publish(&"cached".into()).await?; let mut early_subscriber = early_subscriber; @@ -434,7 +399,7 @@ mod tests { tokio::time::timeout(Duration::from_secs(2), early_subscriber.recv()).await??; let late_subscriber = late_sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .raw() .build() @@ -465,14 +430,10 @@ mod tests { QosHistory::KeepLast(NonZeroUsize::new(10).unwrap()), ); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -505,17 +466,13 @@ mod tests { ); let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; let receive_task = tokio::spawn(collect_messages(subscriber, 10, Duration::from_secs(5))); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; tokio::time::sleep(Duration::from_millis(2000)).await; @@ -548,7 +505,7 @@ mod tests { QosHistory::KeepLast(NonZeroUsize::new(1).unwrap()), ); - let pub_handle = node.publisher::(topic)?.qos(qos).build().await?; + let pub_handle = node.publisher::(topic).qos(qos).build().await?; let message = "Test message".to_string(); let publish_result = @@ -579,17 +536,13 @@ mod tests { ); let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; let receive_task = tokio::spawn(collect_messages(subscriber, 2, Duration::from_secs(4))); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; tokio::time::sleep(Duration::from_millis(1000)).await; @@ -619,14 +572,10 @@ mod tests { QosHistory::KeepLast(NonZeroUsize::new(10).unwrap()), ); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; @@ -679,17 +628,13 @@ mod tests { }; let subscriber = sub_node - .subscriber::(topic)? + .subscriber::(topic) .qos(qos) .build() .await?; let receive_task = tokio::spawn(collect_messages(subscriber, 2, Duration::from_secs(4))); - let pub_handle = pub_node - .publisher::(topic)? - .qos(qos) - .build() - .await?; + let pub_handle = pub_node.publisher::(topic).qos(qos).build().await?; tokio::time::sleep(Duration::from_millis(1000)).await; diff --git a/crates/ros-z/tests/service.rs b/crates/ros-z/tests/service.rs index 4a58961d58..ed82443df0 100644 --- a/crates/ros-z/tests/service.rs +++ b/crates/ros-z/tests/service.rs @@ -52,6 +52,47 @@ fn assert_service_timeout(error: &ros_z::Error, expected_service: &str) { } } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn service_factories_defer_name_errors_to_build() { + let context = ContextBuilder::default() + .disable_multicast_scouting() + .with_json("connect/endpoints", json!([])) + .build() + .await + .expect("Failed to create context"); + let node = context + .create_node("deferred_service_errors") + .build() + .await + .expect("Failed to create node"); + + let server_error = node + .service_server::("bad%service") + .build() + .await + .expect_err("invalid service server name should fail during build"); + assert!(matches!( + server_error, + ros_z::Error::Name { + kind: ros_z::error::NameKind::Service, + .. + } + )); + + let client_error = node + .service_client::("bad%service") + .build() + .await + .expect_err("invalid service client name should fail during build"); + assert!(matches!( + client_error, + ros_z::Error::Name { + kind: ros_z::error::NameKind::Service, + .. + } + )); +} + #[tokio::test(flavor = "multi_thread")] async fn test_basic_service_request_response() { let context = ContextBuilder::default() @@ -71,11 +112,7 @@ async fn test_basic_service_request_response() { .expect("Failed to create node"); let mut server = handle - .block_on( - node.create_service_server::("add_two_ints") - .expect("endpoint factory should succeed") - .build(), - ) + .block_on(node.service_server::("add_two_ints").build()) .expect("Failed to create server"); // Wait for request @@ -99,11 +136,7 @@ async fn test_basic_service_request_response() { .expect("Failed to create node"); let client = handle - .block_on( - node.create_service_client::("add_two_ints") - .expect("endpoint factory should succeed") - .build(), - ) + .block_on(node.service_client::("add_two_ints").build()) .expect("Failed to create client"); // Give server time to start @@ -141,8 +174,7 @@ async fn test_async_service_request_response() { .expect("Failed to create node"); let mut server = node - .create_service_server::("async_add") - .expect("endpoint factory should succeed") + .service_server::("async_add") .build() .await .expect("Failed to create server"); @@ -171,8 +203,7 @@ async fn test_async_service_request_response() { .expect("Failed to create node"); let client = node - .create_service_client::("async_add") - .expect("endpoint factory should succeed") + .service_client::("async_add") .build() .await .expect("Failed to create client"); @@ -213,11 +244,7 @@ async fn test_multiple_service_requests() { .expect("Failed to create node"); let mut server = handle - .block_on( - node.create_service_server::("multi_add") - .expect("endpoint factory should succeed") - .build(), - ) + .block_on(node.service_server::("multi_add").build()) .expect("Failed to create server"); // Handle 3 requests @@ -243,11 +270,7 @@ async fn test_multiple_service_requests() { .expect("Failed to create node"); let client = handle - .block_on( - node.create_service_client::("multi_add") - .expect("endpoint factory should succeed") - .build(), - ) + .block_on(node.service_client::("multi_add").build()) .expect("Failed to create client"); // Give server time to start @@ -289,11 +312,7 @@ async fn test_blocking_call_waits_for_service_response() { .expect("Failed to create node"); let mut server = handle - .block_on( - node.create_service_server::("blocking_call") - .expect("endpoint factory should succeed") - .build(), - ) + .block_on(node.service_server::("blocking_call").build()) .expect("Failed to create server"); let request = server.take_request().expect("Failed to take request"); @@ -315,11 +334,7 @@ async fn test_blocking_call_waits_for_service_response() { .expect("Failed to create node"); let client = handle - .block_on( - node.create_service_client::("blocking_call") - .expect("endpoint factory should succeed") - .build(), - ) + .block_on(node.service_client::("blocking_call").build()) .expect("Failed to create client"); thread::sleep(Duration::from_millis(100)); @@ -356,8 +371,7 @@ async fn test_blocking_call_with_timeout_can_exceed_old_builder_timeout() { let mut server = handle .block_on( - node.create_service_server::("builder_timeout_queue") - .expect("endpoint factory should succeed") + node.service_server::("builder_timeout_queue") .build(), ) .expect("Failed to create server"); @@ -380,8 +394,7 @@ async fn test_blocking_call_with_timeout_can_exceed_old_builder_timeout() { .expect("Failed to create node"); let client = node - .create_service_client::("builder_timeout_queue") - .expect("endpoint factory should succeed") + .service_client::("builder_timeout_queue") .build() .await .expect("Failed to create client"); @@ -418,8 +431,7 @@ async fn test_async_call_with_timeout_can_exceed_old_builder_timeout() { .expect("Failed to create node"); let mut server = node - .create_service_server::("builder_timeout_async") - .expect("endpoint factory should succeed") + .service_server::("builder_timeout_async") .build() .await .expect("Failed to create server"); @@ -447,8 +459,7 @@ async fn test_async_call_with_timeout_can_exceed_old_builder_timeout() { .expect("Failed to create node"); let client = node - .create_service_client::("builder_timeout_async") - .expect("endpoint factory should succeed") + .service_client::("builder_timeout_async") .build() .await .expect("Failed to create client"); @@ -484,8 +495,7 @@ async fn test_blocking_call_with_timeout_reports_timeout_when_no_service_matches .expect("Failed to create node"); let client = node - .create_service_client::("blocking_timeout") - .expect("endpoint factory should succeed") + .service_client::("blocking_timeout") .build() .await .expect("Failed to create client"); @@ -517,8 +527,7 @@ async fn async_call_with_timeout_reports_timeout_when_no_service_matches() { .build() .await .expect("Failed to create node") - .create_service_client::("async_timeout") - .expect("endpoint factory should succeed") + .service_client::("async_timeout") .build() .await .expect("Failed to create client"); @@ -558,8 +567,7 @@ async fn test_blocking_call_with_timeout_returns_response_before_deadline() { let mut server = handle .block_on( - node.create_service_server::("blocking_timeout_success") - .expect("endpoint factory should succeed") + node.service_server::("blocking_timeout_success") .build(), ) .expect("Failed to create server"); @@ -579,8 +587,7 @@ async fn test_blocking_call_with_timeout_returns_response_before_deadline() { .expect("Failed to create node"); let client = node - .create_service_client::("blocking_timeout_success") - .expect("endpoint factory should succeed") + .service_client::("blocking_timeout_success") .build() .await .expect("Failed to create client"); @@ -623,8 +630,7 @@ async fn test_blocking_call_with_timeout_reports_real_timeout_while_waiting_for_ let mut server = handle .block_on( - node.create_service_server::("blocking_timeout_waiting") - .expect("endpoint factory should succeed") + node.service_server::("blocking_timeout_waiting") .build(), ) .expect("Failed to create server"); @@ -644,8 +650,7 @@ async fn test_blocking_call_with_timeout_reports_real_timeout_while_waiting_for_ .expect("Failed to create node"); let client = node - .create_service_client::("blocking_timeout_waiting") - .expect("endpoint factory should succeed") + .service_client::("blocking_timeout_waiting") .build() .await .expect("Failed to create client"); @@ -681,8 +686,7 @@ async fn test_blocking_call_with_timeout_reports_timeout_without_reply() { .build() .await .expect("Failed to create node") - .create_service_server::("blocking_early_completion") - .expect("endpoint factory should succeed") + .service_server::("blocking_early_completion") .build_with_callback(move |_query| { // Intentionally end the query without producing a successful reply sample. }) @@ -698,8 +702,7 @@ async fn test_blocking_call_with_timeout_reports_timeout_without_reply() { .expect("Failed to create node"); let client = node - .create_service_client::("blocking_early_completion") - .expect("endpoint factory should succeed") + .service_client::("blocking_early_completion") .build() .await .expect("Failed to create client"); @@ -733,8 +736,7 @@ async fn async_call_with_timeout_reports_timeout_without_reply() { .build() .await .expect("Failed to create node") - .create_service_server::("async_early_completion") - .expect("endpoint factory should succeed") + .service_server::("async_early_completion") .build_with_callback(move |_query| { // Intentionally end the query without producing a successful reply sample. }) @@ -746,8 +748,7 @@ async fn async_call_with_timeout_reports_timeout_without_reply() { .build() .await .expect("Failed to create node") - .create_service_client::("async_early_completion") - .expect("endpoint factory should succeed") + .service_client::("async_early_completion") .build() .await .expect("Failed to create client"); diff --git a/crates/ros-z/tests/shm.rs b/crates/ros-z/tests/shm.rs index a09483c1d1..bd1dbaeaf0 100644 --- a/crates/ros-z/tests/shm.rs +++ b/crates/ros-z/tests/shm.rs @@ -25,14 +25,12 @@ async fn test_shm_pubsub_large_message() { let publisher = node .publisher::>("shm_test_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher"); let subscriber = node .subscriber::>("shm_test_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -83,14 +81,12 @@ async fn test_shm_pubsub_small_message() { let publisher = node .publisher::>("small_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher"); let subscriber = node .subscriber::>("small_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -139,14 +135,12 @@ async fn test_shm_threshold_boundary() { let publisher = node .publisher::>("boundary_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher"); let subscriber = node .subscriber::>("boundary_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -198,14 +192,12 @@ async fn test_shm_config_hierarchy_node_override() { let publisher = node .publisher::>("hierarchy_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher"); let subscriber = node .subscriber::>("hierarchy_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -248,7 +240,6 @@ async fn test_without_shm() { let publisher = node .publisher::>("no_shm_topic") - .expect("endpoint factory should succeed") .without_shm() // Explicitly disable SHM .build() .await @@ -256,7 +247,6 @@ async fn test_without_shm() { let subscriber = node .subscriber::>("no_shm_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -305,7 +295,6 @@ async fn test_publisher_shm_override() { let publisher = node .publisher::>("pub_shm_topic") - .expect("endpoint factory should succeed") .shm_config(pub_shm_config) .build() .await @@ -313,7 +302,6 @@ async fn test_publisher_shm_override() { let subscriber = node .subscriber::>("pub_shm_topic") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber"); @@ -357,7 +345,6 @@ async fn test_multiple_publishers_different_thresholds() { // Publisher 1: uses context default let pub1 = node .publisher::>("topic1") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create publisher 1"); @@ -370,7 +357,6 @@ async fn test_multiple_publishers_different_thresholds() { ); let pub2 = node .publisher::>("topic2") - .expect("endpoint factory should succeed") .shm_config(ShmConfig::new(provider2).with_threshold(1_000)) .build() .await @@ -378,14 +364,12 @@ async fn test_multiple_publishers_different_thresholds() { let sub1 = node .subscriber::>("topic1") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber 1"); let sub2 = node .subscriber::>("topic2") - .expect("endpoint factory should succeed") .build() .await .expect("Failed to create subscriber 2"); diff --git a/crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.rs b/crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.rs new file mode 100644 index 0000000000..99526a5e74 --- /dev/null +++ b/crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.rs @@ -0,0 +1,35 @@ +use ros_z::{ + context::ContextBuilder, + dynamic::{Schema, StructDef, TypeDef, TypeDefinition, TypeDefinitions, TypeName}, + entity::TypeInfo, +}; + +fn dynamic_schema() -> (TypeInfo, Schema) { + let root = TypeName::new("test_msgs::CompileDynamic").unwrap(); + let schema = std::sync::Arc::new(ros_z_schema::SchemaBundle { + root: TypeDef::Named(root.clone()), + definitions: TypeDefinitions::from([( + root, + TypeDefinition::Struct(StructDef { fields: vec![] }), + )]), + }); + let type_info = TypeInfo::new( + "test_msgs::CompileDynamic", + ros_z_schema::compute_hash(schema.as_ref()).unwrap(), + ); + (type_info, schema) +} + +async fn dynamic_subscriber_does_not_expose_cache() -> ros_z::Result<()> { + let context = ContextBuilder::default().build().await?; + let node = context.create_node("dynamic_cache_compile_api").build().await?; + let (dynamic_type_info, dynamic_schema) = dynamic_schema(); + + let _cache_builder = node + .dynamic_subscriber("compile_dynamic", dynamic_type_info, dynamic_schema) + .cache(4); + + Ok(()) +} + +fn main() {} diff --git a/crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.stderr b/crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.stderr new file mode 100644 index 0000000000..7237aad740 --- /dev/null +++ b/crates/ros-z/tests/ui/endpoint_builder/dynamic_subscriber_cache.stderr @@ -0,0 +1,18 @@ +error[E0599]: the method `cache` exists for struct `SubscriberBuilder`, but its trait bounds were not satisfied + --> tests/ui/endpoint_builder/dynamic_subscriber_cache.rs:30:10 + | +28 | let _cache_builder = node + | __________________________- +29 | | .dynamic_subscriber("compile_dynamic", dynamic_type_info, dynamic_schema) +30 | | .cache(4); + | | -^^^^^ method cannot be called on `SubscriberBuilder` due to unsatisfied trait bounds + | |_________| + | + | + ::: src/dynamic/codec.rs + | + | pub struct DynamicCdrCodec; + | -------------------------- doesn't satisfy `<_ as WireDecoder>::Input<'a> = &[u8]` + | + = note: the following trait bounds were not satisfied: + `::Input<'a> = &'a [u8]` diff --git a/crates/ros-z/tests/ui/endpoint_builder/new_api_pass.rs b/crates/ros-z/tests/ui/endpoint_builder/new_api_pass.rs new file mode 100644 index 0000000000..d5eccc3479 --- /dev/null +++ b/crates/ros-z/tests/ui/endpoint_builder/new_api_pass.rs @@ -0,0 +1,134 @@ +use std::time::Duration; + +use ros_z::{ + Message, ServiceTypeInfo, + context::ContextBuilder, + dynamic::{DynamicPayload, Schema, StructDef, TypeDef, TypeDefinition, TypeDefinitions, TypeName}, + entity::TypeInfo, + message::Service, + qos::QosProfile, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Message)] +#[message(name = "test_msgs::CompileMessage")] +struct CompileMessage { + data: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Message)] +#[message(name = "test_msgs::CompileRequest")] +struct CompileRequest { + value: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Message)] +#[message(name = "test_msgs::CompileResponse")] +struct CompileResponse { + value: u32, +} + +struct CompileService; + +impl Service for CompileService { + type Request = CompileRequest; + type Response = CompileResponse; +} + +impl ServiceTypeInfo for CompileService { + fn service_type_info() -> TypeInfo { + let descriptor = ros_z_schema::ServiceDef::new( + "test_msgs::CompileService", + CompileRequest::type_name(), + CompileResponse::type_name(), + ) + .expect("test service descriptor should be valid"); + let hash = ros_z_schema::compute_hash(&descriptor).expect("test service hash should be valid"); + TypeInfo::new(descriptor.type_name.as_str(), hash) + } +} + +fn dynamic_schema() -> (TypeInfo, Schema) { + let root = TypeName::new("test_msgs::CompileDynamic").unwrap(); + let schema = std::sync::Arc::new(ros_z_schema::SchemaBundle { + root: TypeDef::Named(root.clone()), + definitions: TypeDefinitions::from([( + root, + TypeDefinition::Struct(StructDef { fields: vec![] }), + )]), + }); + let type_info = TypeInfo::new( + "test_msgs::CompileDynamic", + ros_z_schema::compute_hash(schema.as_ref()).unwrap(), + ); + (type_info, schema) +} + +async fn compile_new_endpoint_api() -> ros_z::Result<()> { + let context = ContextBuilder::default().build().await?; + let node = context.create_node("endpoint_compile_api").build().await?; + let qos = QosProfile::default(); + let (dynamic_type_info, dynamic_schema) = dynamic_schema(); + + let _publisher = node + .publisher::("compile_topic") + .qos(qos) + .build() + .await?; + let _subscriber = node + .subscriber::("compile_topic") + .qos(qos) + .build() + .await?; + let _raw = node + .subscriber::("compile_topic") + .qos(qos) + .raw() + .build() + .await?; + let _cache = node + .subscriber::("compile_topic") + .qos(qos) + .cache(4) + .build() + .await?; + let _dynamic_publisher = node + .dynamic_publisher("compile_dynamic", dynamic_type_info.clone(), dynamic_schema.clone()) + .build() + .await?; + let _dynamic_subscriber = node + .dynamic_subscriber("compile_dynamic", dynamic_type_info, dynamic_schema) + .build() + .await?; + let _auto_dynamic_subscriber = node + .dynamic_subscriber_auto("compile_dynamic", Duration::from_millis(1)) + .qos(qos) + .locality(zenoh::sample::Locality::Remote) + .transient_local_replay_timeout(Duration::from_millis(1)) + .build() + .await?; + let _auto_dynamic_raw_subscriber = node + .dynamic_subscriber_auto("compile_dynamic", Duration::from_millis(1)) + .qos(qos) + .locality(zenoh::sample::Locality::Remote) + .transient_local_replay_timeout(Duration::from_millis(1)) + .raw() + .build() + .await?; + let _server = node + .service_server::("compile_service") + .qos(qos) + .build() + .await?; + let _client = node + .service_client::("compile_service") + .qos(qos) + .build() + .await?; + + let _payload_type_check: Option = None; + + Ok(()) +} + +fn main() {}