Skip to main content

use_robot_subsystem/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive robot subsystem vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty robot subsystem name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct RobotSubsystemName(String);
12
13impl RobotSubsystemName {
14    /// Creates a robot subsystem name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`RobotSubsystemTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotSubsystemTextError> {
20        non_empty_subsystem_text(value).map(Self)
21    }
22
23    /// Returns the subsystem 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 RobotSubsystemName {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42impl fmt::Display for RobotSubsystemName {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for RobotSubsystemName {
49    type Err = RobotSubsystemTextError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        Self::new(value)
53    }
54}
55
56/// Descriptive robot subsystem kind vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum RobotSubsystemKind {
59    /// Locomotion subsystem label.
60    Locomotion,
61    /// Manipulation subsystem label.
62    Manipulation,
63    /// Perception subsystem label.
64    Perception,
65    /// Power subsystem label.
66    Power,
67    /// Control subsystem label.
68    Control,
69    /// Communication subsystem label.
70    Communication,
71    /// Navigation subsystem label.
72    Navigation,
73    /// Safety subsystem label.
74    Safety,
75    /// Human-interface subsystem label.
76    HumanInterface,
77    /// Unknown subsystem kind.
78    Unknown,
79    /// Caller-defined subsystem kind text.
80    Custom(String),
81}
82
83impl fmt::Display for RobotSubsystemKind {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str(match self {
86            Self::Locomotion => "locomotion",
87            Self::Manipulation => "manipulation",
88            Self::Perception => "perception",
89            Self::Power => "power",
90            Self::Control => "control",
91            Self::Communication => "communication",
92            Self::Navigation => "navigation",
93            Self::Safety => "safety",
94            Self::HumanInterface => "human-interface",
95            Self::Unknown => "unknown",
96            Self::Custom(value) => value.as_str(),
97        })
98    }
99}
100
101impl FromStr for RobotSubsystemKind {
102    type Err = RobotSubsystemKindParseError;
103
104    fn from_str(value: &str) -> Result<Self, Self::Err> {
105        let trimmed = value.trim();
106        if trimmed.is_empty() {
107            return Err(RobotSubsystemKindParseError::Empty);
108        }
109
110        match normalized_token(trimmed).as_str() {
111            "locomotion" => Ok(Self::Locomotion),
112            "manipulation" => Ok(Self::Manipulation),
113            "perception" => Ok(Self::Perception),
114            "power" => Ok(Self::Power),
115            "control" => Ok(Self::Control),
116            "communication" | "communications" => Ok(Self::Communication),
117            "navigation" => Ok(Self::Navigation),
118            "safety" => Ok(Self::Safety),
119            "human-interface" | "hmi" => Ok(Self::HumanInterface),
120            "unknown" => Ok(Self::Unknown),
121            _ => Ok(Self::Custom(trimmed.to_string())),
122        }
123    }
124}
125
126/// Descriptive subsystem lifecycle or status vocabulary.
127#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
128pub enum SubsystemState {
129    /// Offline subsystem state.
130    Offline,
131    /// Starting subsystem state.
132    Starting,
133    /// Ready subsystem state.
134    Ready,
135    /// Active subsystem state.
136    Active,
137    /// Degraded subsystem state.
138    Degraded,
139    /// Faulted subsystem state.
140    Faulted,
141    /// Stopped subsystem state.
142    Stopped,
143    /// Unknown subsystem state.
144    Unknown,
145    /// Caller-defined subsystem state text.
146    Custom(String),
147}
148
149impl fmt::Display for SubsystemState {
150    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
151        formatter.write_str(match self {
152            Self::Offline => "offline",
153            Self::Starting => "starting",
154            Self::Ready => "ready",
155            Self::Active => "active",
156            Self::Degraded => "degraded",
157            Self::Faulted => "faulted",
158            Self::Stopped => "stopped",
159            Self::Unknown => "unknown",
160            Self::Custom(value) => value.as_str(),
161        })
162    }
163}
164
165impl FromStr for SubsystemState {
166    type Err = SubsystemStateParseError;
167
168    fn from_str(value: &str) -> Result<Self, Self::Err> {
169        let trimmed = value.trim();
170        if trimmed.is_empty() {
171            return Err(SubsystemStateParseError::Empty);
172        }
173
174        match normalized_token(trimmed).as_str() {
175            "offline" => Ok(Self::Offline),
176            "starting" => Ok(Self::Starting),
177            "ready" => Ok(Self::Ready),
178            "active" => Ok(Self::Active),
179            "degraded" => Ok(Self::Degraded),
180            "faulted" | "fault" => Ok(Self::Faulted),
181            "stopped" | "stop" => Ok(Self::Stopped),
182            "unknown" => Ok(Self::Unknown),
183            _ => Ok(Self::Custom(trimmed.to_string())),
184        }
185    }
186}
187
188/// Errors returned while constructing subsystem text values.
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub enum RobotSubsystemTextError {
191    /// The value was empty after trimming whitespace.
192    Empty,
193}
194
195impl fmt::Display for RobotSubsystemTextError {
196    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
197        match self {
198            Self::Empty => formatter.write_str("robot subsystem text cannot be empty"),
199        }
200    }
201}
202
203impl Error for RobotSubsystemTextError {}
204
205/// Error returned when parsing subsystem kinds fails.
206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum RobotSubsystemKindParseError {
208    /// The subsystem kind was empty after trimming whitespace.
209    Empty,
210}
211
212impl fmt::Display for RobotSubsystemKindParseError {
213    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match self {
215            Self::Empty => formatter.write_str("robot subsystem kind cannot be empty"),
216        }
217    }
218}
219
220impl Error for RobotSubsystemKindParseError {}
221
222/// Error returned when parsing subsystem states fails.
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub enum SubsystemStateParseError {
225    /// The subsystem state was empty after trimming whitespace.
226    Empty,
227}
228
229impl fmt::Display for SubsystemStateParseError {
230    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231        match self {
232            Self::Empty => formatter.write_str("subsystem state cannot be empty"),
233        }
234    }
235}
236
237impl Error for SubsystemStateParseError {}
238
239fn non_empty_subsystem_text(value: impl AsRef<str>) -> Result<String, RobotSubsystemTextError> {
240    let trimmed = value.as_ref().trim();
241
242    if trimmed.is_empty() {
243        Err(RobotSubsystemTextError::Empty)
244    } else {
245        Ok(trimmed.to_string())
246    }
247}
248
249fn normalized_token(value: &str) -> String {
250    value
251        .trim()
252        .chars()
253        .map(|character| match character {
254            '_' | ' ' => '-',
255            other => other.to_ascii_lowercase(),
256        })
257        .collect()
258}
259
260#[cfg(test)]
261mod tests {
262    use super::{
263        RobotSubsystemKind, RobotSubsystemKindParseError, RobotSubsystemName,
264        RobotSubsystemTextError, SubsystemState, SubsystemStateParseError,
265    };
266
267    #[test]
268    fn constructs_valid_subsystem_name() -> Result<(), RobotSubsystemTextError> {
269        let name = RobotSubsystemName::new("  arm  ")?;
270
271        assert_eq!(name.as_str(), "arm");
272        Ok(())
273    }
274
275    #[test]
276    fn rejects_empty_subsystem_name() {
277        assert_eq!(
278            RobotSubsystemName::new(""),
279            Err(RobotSubsystemTextError::Empty)
280        );
281    }
282
283    #[test]
284    fn displays_and_parses_subsystem_kind() -> Result<(), RobotSubsystemKindParseError> {
285        assert_eq!(
286            "human interface".parse::<RobotSubsystemKind>()?,
287            RobotSubsystemKind::HumanInterface
288        );
289        assert_eq!(RobotSubsystemKind::Perception.to_string(), "perception");
290        Ok(())
291    }
292
293    #[test]
294    fn displays_and_parses_subsystem_state() -> Result<(), SubsystemStateParseError> {
295        assert_eq!("ready".parse::<SubsystemState>()?, SubsystemState::Ready);
296        assert_eq!(SubsystemState::Degraded.to_string(), "degraded");
297        Ok(())
298    }
299
300    #[test]
301    fn stores_custom_subsystem_kind() -> Result<(), RobotSubsystemKindParseError> {
302        assert_eq!(
303            "payload-handling".parse::<RobotSubsystemKind>()?,
304            RobotSubsystemKind::Custom("payload-handling".to_string())
305        );
306        Ok(())
307    }
308}