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 RobotSubsystemName(String);
12
13impl RobotSubsystemName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, RobotSubsystemTextError> {
20 non_empty_subsystem_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 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum RobotSubsystemKind {
59 Locomotion,
61 Manipulation,
63 Perception,
65 Power,
67 Control,
69 Communication,
71 Navigation,
73 Safety,
75 HumanInterface,
77 Unknown,
79 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
128pub enum SubsystemState {
129 Offline,
131 Starting,
133 Ready,
135 Active,
137 Degraded,
139 Faulted,
141 Stopped,
143 Unknown,
145 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub enum RobotSubsystemTextError {
191 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub enum RobotSubsystemKindParseError {
208 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub enum SubsystemStateParseError {
225 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}