Skip to main content

use_robot_sensor/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive robot sensor vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty robot sensor name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct RobotSensorName(String);
12
13impl RobotSensorName {
14    /// Creates a sensor name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`RobotSensorTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotSensorTextError> {
20        non_empty_sensor_text(value).map(Self)
21    }
22
23    /// Returns the sensor name text.
24    #[must_use]
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28
29    /// Consumes the name and returns the owned string.
30    #[must_use]
31    pub fn into_string(self) -> String {
32        self.0
33    }
34}
35
36impl AsRef<str> for RobotSensorName {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42impl fmt::Display for RobotSensorName {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for RobotSensorName {
49    type Err = RobotSensorTextError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        Self::new(value)
53    }
54}
55
56/// Descriptive robot sensor kind vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum RobotSensorKind {
59    /// Camera sensor.
60    Camera,
61    /// Lidar sensor.
62    Lidar,
63    /// Radar sensor.
64    Radar,
65    /// Inertial measurement unit.
66    Imu,
67    /// Encoder sensor.
68    Encoder,
69    /// Force-torque sensor.
70    ForceTorque,
71    /// Proximity sensor.
72    Proximity,
73    /// Ultrasonic sensor.
74    Ultrasonic,
75    /// GPS receiver.
76    Gps,
77    /// Microphone sensor.
78    Microphone,
79    /// Unknown sensor kind.
80    Unknown,
81    /// Caller-defined sensor kind text.
82    Custom(String),
83}
84
85impl fmt::Display for RobotSensorKind {
86    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87        formatter.write_str(match self {
88            Self::Camera => "camera",
89            Self::Lidar => "lidar",
90            Self::Radar => "radar",
91            Self::Imu => "imu",
92            Self::Encoder => "encoder",
93            Self::ForceTorque => "force-torque",
94            Self::Proximity => "proximity",
95            Self::Ultrasonic => "ultrasonic",
96            Self::Gps => "gps",
97            Self::Microphone => "microphone",
98            Self::Unknown => "unknown",
99            Self::Custom(value) => value.as_str(),
100        })
101    }
102}
103
104impl FromStr for RobotSensorKind {
105    type Err = RobotSensorKindParseError;
106
107    fn from_str(value: &str) -> Result<Self, Self::Err> {
108        let trimmed = value.trim();
109        if trimmed.is_empty() {
110            return Err(RobotSensorKindParseError::Empty);
111        }
112
113        match normalized_token(trimmed).as_str() {
114            "camera" => Ok(Self::Camera),
115            "lidar" | "laser-scanner" => Ok(Self::Lidar),
116            "radar" => Ok(Self::Radar),
117            "imu" | "inertial-measurement-unit" => Ok(Self::Imu),
118            "encoder" => Ok(Self::Encoder),
119            "force-torque" | "ft" => Ok(Self::ForceTorque),
120            "proximity" => Ok(Self::Proximity),
121            "ultrasonic" => Ok(Self::Ultrasonic),
122            "gps" | "gnss" => Ok(Self::Gps),
123            "microphone" | "mic" => Ok(Self::Microphone),
124            "unknown" => Ok(Self::Unknown),
125            _ => Ok(Self::Custom(trimmed.to_string())),
126        }
127    }
128}
129
130/// A non-empty descriptive sensor mount label.
131#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct SensorMount(String);
133
134impl SensorMount {
135    /// Creates a sensor mount label from non-empty text.
136    ///
137    /// # Errors
138    ///
139    /// Returns [`RobotSensorTextError::Empty`] when the trimmed mount label is empty.
140    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotSensorTextError> {
141        non_empty_sensor_text(value).map(Self)
142    }
143
144    /// Returns the mount label text.
145    #[must_use]
146    pub fn as_str(&self) -> &str {
147        &self.0
148    }
149
150    /// Consumes the mount label and returns the owned string.
151    #[must_use]
152    pub fn into_string(self) -> String {
153        self.0
154    }
155}
156
157impl AsRef<str> for SensorMount {
158    fn as_ref(&self) -> &str {
159        self.as_str()
160    }
161}
162
163impl fmt::Display for SensorMount {
164    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
165        formatter.write_str(self.as_str())
166    }
167}
168
169impl FromStr for SensorMount {
170    type Err = RobotSensorTextError;
171
172    fn from_str(value: &str) -> Result<Self, Self::Err> {
173        Self::new(value)
174    }
175}
176
177/// Descriptive sensor reading kind vocabulary.
178#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179pub enum SensorReadingKind {
180    /// Image reading label.
181    Image,
182    /// Point cloud reading label.
183    PointCloud,
184    /// Distance reading label.
185    Distance,
186    /// Acceleration reading label.
187    Acceleration,
188    /// Angular velocity reading label.
189    AngularVelocity,
190    /// Position reading label.
191    Position,
192    /// Force reading label.
193    Force,
194    /// Torque reading label.
195    Torque,
196    /// Contact reading label.
197    Contact,
198    /// Unknown reading kind.
199    Unknown,
200    /// Caller-defined reading kind text.
201    Custom(String),
202}
203
204impl fmt::Display for SensorReadingKind {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        formatter.write_str(match self {
207            Self::Image => "image",
208            Self::PointCloud => "point-cloud",
209            Self::Distance => "distance",
210            Self::Acceleration => "acceleration",
211            Self::AngularVelocity => "angular-velocity",
212            Self::Position => "position",
213            Self::Force => "force",
214            Self::Torque => "torque",
215            Self::Contact => "contact",
216            Self::Unknown => "unknown",
217            Self::Custom(value) => value.as_str(),
218        })
219    }
220}
221
222impl FromStr for SensorReadingKind {
223    type Err = SensorReadingKindParseError;
224
225    fn from_str(value: &str) -> Result<Self, Self::Err> {
226        let trimmed = value.trim();
227        if trimmed.is_empty() {
228            return Err(SensorReadingKindParseError::Empty);
229        }
230
231        match normalized_token(trimmed).as_str() {
232            "image" => Ok(Self::Image),
233            "point-cloud" => Ok(Self::PointCloud),
234            "distance" => Ok(Self::Distance),
235            "acceleration" => Ok(Self::Acceleration),
236            "angular-velocity" => Ok(Self::AngularVelocity),
237            "position" => Ok(Self::Position),
238            "force" => Ok(Self::Force),
239            "torque" => Ok(Self::Torque),
240            "contact" => Ok(Self::Contact),
241            "unknown" => Ok(Self::Unknown),
242            _ => Ok(Self::Custom(trimmed.to_string())),
243        }
244    }
245}
246
247/// Errors returned while constructing robot sensor text values.
248#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum RobotSensorTextError {
250    /// The value was empty after trimming whitespace.
251    Empty,
252}
253
254impl fmt::Display for RobotSensorTextError {
255    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256        match self {
257            Self::Empty => formatter.write_str("robot sensor text cannot be empty"),
258        }
259    }
260}
261
262impl Error for RobotSensorTextError {}
263
264/// Error returned when parsing sensor kinds fails.
265#[derive(Clone, Copy, Debug, Eq, PartialEq)]
266pub enum RobotSensorKindParseError {
267    /// The sensor kind was empty after trimming whitespace.
268    Empty,
269}
270
271impl fmt::Display for RobotSensorKindParseError {
272    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
273        match self {
274            Self::Empty => formatter.write_str("robot sensor kind cannot be empty"),
275        }
276    }
277}
278
279impl Error for RobotSensorKindParseError {}
280
281/// Error returned when parsing reading kinds fails.
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
283pub enum SensorReadingKindParseError {
284    /// The reading kind was empty after trimming whitespace.
285    Empty,
286}
287
288impl fmt::Display for SensorReadingKindParseError {
289    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
290        match self {
291            Self::Empty => formatter.write_str("sensor reading kind cannot be empty"),
292        }
293    }
294}
295
296impl Error for SensorReadingKindParseError {}
297
298fn non_empty_sensor_text(value: impl AsRef<str>) -> Result<String, RobotSensorTextError> {
299    let trimmed = value.as_ref().trim();
300
301    if trimmed.is_empty() {
302        Err(RobotSensorTextError::Empty)
303    } else {
304        Ok(trimmed.to_string())
305    }
306}
307
308fn normalized_token(value: &str) -> String {
309    value
310        .trim()
311        .chars()
312        .map(|character| match character {
313            '_' | ' ' => '-',
314            other => other.to_ascii_lowercase(),
315        })
316        .collect()
317}
318
319#[cfg(test)]
320mod tests {
321    use super::{
322        RobotSensorKind, RobotSensorKindParseError, RobotSensorName, RobotSensorTextError,
323        SensorReadingKind, SensorReadingKindParseError,
324    };
325
326    #[test]
327    fn constructs_valid_sensor_name() -> Result<(), RobotSensorTextError> {
328        let name = RobotSensorName::new("  wrist-camera  ")?;
329
330        assert_eq!(name.as_str(), "wrist-camera");
331        Ok(())
332    }
333
334    #[test]
335    fn rejects_empty_sensor_name() {
336        assert_eq!(RobotSensorName::new(""), Err(RobotSensorTextError::Empty));
337    }
338
339    #[test]
340    fn displays_and_parses_sensor_kind() -> Result<(), RobotSensorKindParseError> {
341        assert_eq!(
342            "force torque".parse::<RobotSensorKind>()?,
343            RobotSensorKind::ForceTorque
344        );
345        assert_eq!(RobotSensorKind::Camera.to_string(), "camera");
346        Ok(())
347    }
348
349    #[test]
350    fn displays_and_parses_reading_kind() -> Result<(), SensorReadingKindParseError> {
351        assert_eq!(
352            "point cloud".parse::<SensorReadingKind>()?,
353            SensorReadingKind::PointCloud
354        );
355        assert_eq!(
356            SensorReadingKind::AngularVelocity.to_string(),
357            "angular-velocity"
358        );
359        Ok(())
360    }
361
362    #[test]
363    fn stores_custom_sensor_kind() -> Result<(), RobotSensorKindParseError> {
364        assert_eq!(
365            "event-camera".parse::<RobotSensorKind>()?,
366            RobotSensorKind::Custom("event-camera".to_string())
367        );
368        Ok(())
369    }
370}