Skip to main content

use_robot/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive robot vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty robot name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct RobotName(String);
12
13impl RobotName {
14    /// Creates a robot name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`RobotTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
20        non_empty_robot_text(value).map(Self)
21    }
22
23    /// Returns the robot 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 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/// Broad robot vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum RobotKind {
59    /// Robot arm.
60    Arm,
61    /// Mobile robot.
62    Mobile,
63    /// Humanoid robot.
64    Humanoid,
65    /// Flying robot or drone.
66    Drone,
67    /// Quadruped robot.
68    Quadruped,
69    /// Manipulator robot.
70    Manipulator,
71    /// Collaborative robot.
72    Collaborative,
73    /// Industrial robot.
74    Industrial,
75    /// Educational robot.
76    Educational,
77    /// Unknown robot kind.
78    Unknown,
79    /// Caller-defined robot kind text.
80    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/// A non-empty robot model label.
127#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
128pub struct RobotModel(String);
129
130impl RobotModel {
131    /// Creates a robot model from non-empty text.
132    ///
133    /// # Errors
134    ///
135    /// Returns [`RobotTextError::Empty`] when the trimmed model is empty.
136    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
137        non_empty_robot_text(value).map(Self)
138    }
139
140    /// Returns the model text.
141    #[must_use]
142    pub fn as_str(&self) -> &str {
143        &self.0
144    }
145
146    /// Consumes the model and returns the owned string.
147    #[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/// A descriptive robot manufacturer label.
174#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
175pub struct RobotManufacturer(String);
176
177impl RobotManufacturer {
178    /// Creates a manufacturer label from non-empty text.
179    ///
180    /// # Errors
181    ///
182    /// Returns [`RobotTextError::Empty`] when the trimmed manufacturer is empty.
183    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
184        non_empty_robot_text(value).map(Self)
185    }
186
187    /// Returns the manufacturer text.
188    #[must_use]
189    pub fn as_str(&self) -> &str {
190        &self.0
191    }
192
193    /// Consumes the manufacturer and returns the owned string.
194    #[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/// Errors returned while constructing robot text values.
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum RobotTextError {
223    /// The value was empty after trimming whitespace.
224    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/// Error returned when parsing robot kinds fails.
238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub enum RobotKindParseError {
240    /// The robot kind was empty after trimming whitespace.
241    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}