1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
7use std::error::Error;
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct RobotSensorName(String);
12
13impl RobotSensorName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, RobotSensorTextError> {
20 non_empty_sensor_text(value).map(Self)
21 }
22
23 #[must_use]
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28
29 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum RobotSensorKind {
59 Camera,
61 Lidar,
63 Radar,
65 Imu,
67 Encoder,
69 ForceTorque,
71 Proximity,
73 Ultrasonic,
75 Gps,
77 Microphone,
79 Unknown,
81 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
132pub struct SensorMount(String);
133
134impl SensorMount {
135 pub fn new(value: impl AsRef<str>) -> Result<Self, RobotSensorTextError> {
141 non_empty_sensor_text(value).map(Self)
142 }
143
144 #[must_use]
146 pub fn as_str(&self) -> &str {
147 &self.0
148 }
149
150 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179pub enum SensorReadingKind {
180 Image,
182 PointCloud,
184 Distance,
186 Acceleration,
188 AngularVelocity,
190 Position,
192 Force,
194 Torque,
196 Contact,
198 Unknown,
200 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
249pub enum RobotSensorTextError {
250 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
266pub enum RobotSensorKindParseError {
267 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
283pub enum SensorReadingKindParseError {
284 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}