shapes/
main.rs

1//! # RTI Connector for Rust example for Shape types
2//!
3//! This example demonstrates how to use the RTI Connector for Rust
4//! to publish and subscribe to Shape data types in a DDS domain.
5//!
6//! ## Usage
7//!
8//! It uses a command-line interface to allow users to choose
9//! between publishing and subscribing modes, as well as configure
10//! parameters such as the number of samples and timeouts.
11//!
12//! ```console
13#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/shapes/help_main.txt"))]
14//! ```
15//!
16//! ### Publisher Command
17//!
18//! Publishes samples of [ShapeType][ShapeType] data at specified intervals
19//!
20//! It can be invoked from the command line as follows:
21//! ```console
22#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/shapes/help_pub.txt"))]
23//! ```
24//!
25//! ### Subscriber Command
26//!
27//! Subscribes to samples of [`ShapeType`][ShapeType] data and prints
28//! them to the console
29//!
30//! It can be invoked from the command line as follows:
31//! ```console
32#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/shapes/help_sub.txt"))]
33//! ```
34//!
35//! ## XML Configuration
36//!
37//! The example uses an XML configuration file (`Shapes.xml`) with the following content:
38//! ```xml
39#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/shapes/Shapes.xml"))]
40//! ```
41//!
42
43#![deny(missing_docs)]
44
45mod publisher;
46mod subscriber;
47
48const PUB_PARTICIPANT_NAME: &str = "ShapeParticipantLibrary::Pub";
49const SUB_PARTICIPANT_NAME: &str = "ShapeParticipantLibrary::Sub";
50const OUTPUT_NAME: &str = "ShapePublisher::ShapeSquareWriter";
51const INPUT_NAME: &str = "ShapeSubscriber::ShapeSquareReader";
52
53use clap::{Parser, Subcommand};
54use serde::{Deserialize, Serialize};
55
56type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
57
58fn validate_samples(s: &str) -> std::result::Result<usize, String> {
59    let value: usize = s
60        .parse()
61        .map_err(|_| format!("`{s}` isn't a valid number"))?;
62    if value == 0 {
63        Err("samples must be greater than 0".to_string())
64    } else {
65        Ok(value)
66    }
67}
68
69/// Indicates whether typed serialization is enabled or disabled
70#[derive(Debug, Clone, Copy)]
71pub enum TypedMode {
72    /// Use JSON serialization when enabled
73    Enabled,
74    /// Use dynamic, by-field serialization when disabled
75    Disabled,
76}
77
78/// Structure matching the ShapeType DDS data model.
79#[derive(Serialize, Deserialize, Debug, Clone)]
80pub struct ShapeType {
81    /// The color of the shape (used as the key field)
82    pub color: String,
83    /// The X coordinate of the shape
84    pub x: i64,
85    /// The Y coordinate of the shape
86    pub y: i64,
87    /// The size of the shape
88    pub shapesize: i64,
89}
90
91/// Command-line arguments for the shapes example application
92#[derive(Parser)]
93#[command(name = "shapes")]
94#[command(about = "RTI Connector for Rust example for Shape data")]
95struct Args {
96    #[command(subcommand)]
97    /// Command to execute (publish or subscribe)
98    command: Commands,
99
100    #[arg(long)]
101    /// Enable typed mode for shapes
102    typed: bool,
103}
104
105impl Args {
106    fn typed_mode(&self) -> TypedMode {
107        if self.typed {
108            TypedMode::Enabled
109        } else {
110            TypedMode::Disabled
111        }
112    }
113}
114
115/// Specific command-line arguments for components of the shapes example
116#[derive(Subcommand)]
117enum Commands {
118    /// Publish shape data to DDS
119    Pub {
120        #[arg(short = 's', long, default_value_t = usize::MAX, value_parser = validate_samples)]
121        /// Total number of samples to publish
122        samples: usize,
123
124        #[arg(short = 'w', long, default_value_t = 200)]
125        /// Sleep duration between samples in milliseconds (0 = no wait)
126        wait_ms: u64,
127
128        #[arg(short = 'd', long, default_value_t = 3000)]
129        /// Wait for subscriptions timeout in milliseconds (0 = infinite)
130        wait_for_subscriptions_ms: u64,
131    },
132    /// Subscribe to shape data from DDS
133    Sub {
134        #[arg(short = 's', long, default_value_t = usize::MAX, value_parser = validate_samples)]
135        /// Total number of samples to read
136        samples: usize,
137
138        #[arg(short = 'w', long, default_value_t = 500)]
139        /// Wait timeout in milliseconds (0 = infinite)
140        wait_ms: u64,
141
142        #[arg(short = 'd', long, default_value_t = 3000)]
143        /// Wait for publications timeout in milliseconds (0 = infinite)
144        wait_for_publications_ms: u64,
145    },
146}
147
148// Shared utility functions
149fn config_path() -> Result<std::path::PathBuf> {
150    use std::{env, fs};
151
152    let contents = include_str!(concat!(
153        env!("CARGO_MANIFEST_DIR"),
154        "/examples/shapes/Shapes.xml"
155    ));
156
157    // Create a temporary file with the XML configuration
158    // Create temp file in system temp directory
159    let temp_dir = env::temp_dir();
160    let temp_path = temp_dir.join("Shapes.xml");
161
162    // Write contents to temp file
163    fs::write(&temp_path, contents)?;
164
165    Ok(temp_path)
166}
167
168fn optional_duration_from_ms(ms: u64) -> Option<std::time::Duration> {
169    if ms == 0 {
170        None
171    } else {
172        Some(std::time::Duration::from_millis(ms))
173    }
174}
175
176fn main() -> Result<()> {
177    run(Args::parse())
178}
179
180fn run(args: Args) -> Result<()> {
181    let typed_mode = args.typed_mode();
182    println!(
183        "Running with typed support: {}",
184        matches!(typed_mode, TypedMode::Enabled)
185    );
186
187    match args.command {
188        Commands::Pub {
189            samples,
190            wait_ms,
191            wait_for_subscriptions_ms,
192        } => publisher::main(typed_mode, samples, wait_ms, wait_for_subscriptions_ms),
193        Commands::Sub {
194            samples,
195            wait_ms,
196            wait_for_publications_ms,
197        } => subscriber::main(typed_mode, samples, wait_ms, wait_for_publications_ms),
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::{Args, Parser, TypedMode, run};
204    use std::thread;
205
206    /// The purpose of this test is to actually execute the shapes example code to ensure it works as expected. It's not a unit test of individual components, but rather an integration test to validate the overall functionality.
207    #[test]
208    fn use_shapes_example() {
209        impl_shapes_example(TypedMode::Disabled);
210        impl_shapes_example(TypedMode::Enabled);
211    }
212
213    fn impl_shapes_example(typed_mode: TypedMode) {
214        let typed_flag = matches!(typed_mode, TypedMode::Enabled);
215
216        // Prepare the threads operations for publisher and subscriber
217        let pub_fn = move || {
218            let args = if typed_flag {
219                "pub --samples 10 --typed"
220            } else {
221                "pub --samples 10"
222            };
223            let args = {
224                let program_iter = std::iter::once("shapes");
225                let args_iter = args.split_whitespace();
226                Args::try_parse_from(program_iter.chain(args_iter))
227            }
228            .expect("Failed to parse publisher arguments");
229
230            run(args).expect("Publisher run should succeed");
231        };
232        let sub_fn = move || {
233            let args = if typed_flag {
234                "sub --samples 10 --typed"
235            } else {
236                "sub --samples 10"
237            };
238            let args = {
239                let program_iter = std::iter::once("shapes");
240                let args_iter = args.split_whitespace();
241                Args::try_parse_from(program_iter.chain(args_iter))
242            }
243            .expect("Failed to parse subscriber arguments");
244
245            run(args).expect("Subscriber run should succeed");
246        };
247
248        // Prepare and run the threads
249        let pub_thread_handle = thread::spawn(move || pub_fn());
250        let sub_thread_handle = thread::spawn(move || sub_fn());
251
252        // Wait for both threads to complete
253        pub_thread_handle
254            .join()
255            .expect("Publisher thread has panicked");
256        sub_thread_handle
257            .join()
258            .expect("Subscriber thread has panicked");
259    }
260}