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 RobotName(String);
12
13impl RobotName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
20 non_empty_robot_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 RobotName {
37 fn as_ref(&self) -> &str {
38 self.as_str()
39 }
40}
41
42impl fmt::Display for RobotName {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter.write_str(self.as_str())
45 }
46}
47
48impl FromStr for RobotName {
49 type Err = RobotTextError;
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 RobotKind {
59 Arm,
61 Mobile,
63 Humanoid,
65 Drone,
67 Quadruped,
69 Manipulator,
71 Collaborative,
73 Industrial,
75 Educational,
77 Unknown,
79 Custom(String),
81}
82
83impl fmt::Display for RobotKind {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter.write_str(match self {
86 Self::Arm => "arm",
87 Self::Mobile => "mobile",
88 Self::Humanoid => "humanoid",
89 Self::Drone => "drone",
90 Self::Quadruped => "quadruped",
91 Self::Manipulator => "manipulator",
92 Self::Collaborative => "collaborative",
93 Self::Industrial => "industrial",
94 Self::Educational => "educational",
95 Self::Unknown => "unknown",
96 Self::Custom(value) => value.as_str(),
97 })
98 }
99}
100
101impl FromStr for RobotKind {
102 type Err = RobotKindParseError;
103
104 fn from_str(value: &str) -> Result<Self, Self::Err> {
105 let trimmed = value.trim();
106 if trimmed.is_empty() {
107 return Err(RobotKindParseError::Empty);
108 }
109
110 match normalized_token(trimmed).as_str() {
111 "arm" | "robot-arm" => Ok(Self::Arm),
112 "mobile" | "mobile-robot" => Ok(Self::Mobile),
113 "humanoid" => Ok(Self::Humanoid),
114 "drone" | "uav" => Ok(Self::Drone),
115 "quadruped" => Ok(Self::Quadruped),
116 "manipulator" => Ok(Self::Manipulator),
117 "collaborative" | "cobot" => Ok(Self::Collaborative),
118 "industrial" => Ok(Self::Industrial),
119 "educational" => Ok(Self::Educational),
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 struct RobotModel(String);
129
130impl RobotModel {
131 pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
137 non_empty_robot_text(value).map(Self)
138 }
139
140 #[must_use]
142 pub fn as_str(&self) -> &str {
143 &self.0
144 }
145
146 #[must_use]
148 pub fn into_string(self) -> String {
149 self.0
150 }
151}
152
153impl AsRef<str> for RobotModel {
154 fn as_ref(&self) -> &str {
155 self.as_str()
156 }
157}
158
159impl fmt::Display for RobotModel {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 formatter.write_str(self.as_str())
162 }
163}
164
165impl FromStr for RobotModel {
166 type Err = RobotTextError;
167
168 fn from_str(value: &str) -> Result<Self, Self::Err> {
169 Self::new(value)
170 }
171}
172
173#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
175pub struct RobotManufacturer(String);
176
177impl RobotManufacturer {
178 pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
184 non_empty_robot_text(value).map(Self)
185 }
186
187 #[must_use]
189 pub fn as_str(&self) -> &str {
190 &self.0
191 }
192
193 #[must_use]
195 pub fn into_string(self) -> String {
196 self.0
197 }
198}
199
200impl AsRef<str> for RobotManufacturer {
201 fn as_ref(&self) -> &str {
202 self.as_str()
203 }
204}
205
206impl fmt::Display for RobotManufacturer {
207 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
208 formatter.write_str(self.as_str())
209 }
210}
211
212impl FromStr for RobotManufacturer {
213 type Err = RobotTextError;
214
215 fn from_str(value: &str) -> Result<Self, Self::Err> {
216 Self::new(value)
217 }
218}
219
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum RobotTextError {
223 Empty,
225}
226
227impl fmt::Display for RobotTextError {
228 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229 match self {
230 Self::Empty => formatter.write_str("robot text cannot be empty"),
231 }
232 }
233}
234
235impl Error for RobotTextError {}
236
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub enum RobotKindParseError {
240 Empty,
242}
243
244impl fmt::Display for RobotKindParseError {
245 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
246 match self {
247 Self::Empty => formatter.write_str("robot kind cannot be empty"),
248 }
249 }
250}
251
252impl Error for RobotKindParseError {}
253
254fn non_empty_robot_text(value: impl AsRef<str>) -> Result<String, RobotTextError> {
255 let trimmed = value.as_ref().trim();
256
257 if trimmed.is_empty() {
258 Err(RobotTextError::Empty)
259 } else {
260 Ok(trimmed.to_string())
261 }
262}
263
264fn normalized_token(value: &str) -> String {
265 value
266 .trim()
267 .chars()
268 .map(|character| match character {
269 '_' | ' ' => '-',
270 other => other.to_ascii_lowercase(),
271 })
272 .collect()
273}
274
275#[cfg(test)]
276mod tests {
277 use super::{RobotKind, RobotKindParseError, RobotModel, RobotName, RobotTextError};
278
279 #[test]
280 fn constructs_valid_robot_name() -> Result<(), RobotTextError> {
281 let name = RobotName::new(" Atlas ")?;
282
283 assert_eq!(name.as_str(), "Atlas");
284 assert_eq!(name.to_string(), "Atlas");
285 Ok(())
286 }
287
288 #[test]
289 fn rejects_empty_robot_name() {
290 assert_eq!(RobotName::new(" "), Err(RobotTextError::Empty));
291 }
292
293 #[test]
294 fn displays_and_parses_robot_kind() -> Result<(), RobotKindParseError> {
295 assert_eq!("robot arm".parse::<RobotKind>()?, RobotKind::Arm);
296 assert_eq!(RobotKind::Collaborative.to_string(), "collaborative");
297 Ok(())
298 }
299
300 #[test]
301 fn stores_custom_robot_kind() -> Result<(), RobotKindParseError> {
302 assert_eq!(
303 "pipe-crawler".parse::<RobotKind>()?,
304 RobotKind::Custom("pipe-crawler".to_string())
305 );
306 Ok(())
307 }
308
309 #[test]
310 fn constructs_robot_model() -> Result<(), RobotTextError> {
311 let model = RobotModel::new("RX-4")?;
312
313 assert_eq!(model.as_str(), "RX-4");
314 Ok(())
315 }
316}