Skip to main content

use_robot_id/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive robot identity vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty robot identifier.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct RobotId(String);
12
13impl RobotId {
14    /// Creates a robot ID from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`RobotIdError::Empty`] when the trimmed identifier is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotIdError> {
20        non_empty_identifier(value).map(Self)
21    }
22
23    /// Returns the identifier text.
24    #[must_use]
25    pub fn as_str(&self) -> &str {
26        &self.0
27    }
28
29    /// Consumes the ID 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 RobotId {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42impl fmt::Display for RobotId {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for RobotId {
49    type Err = RobotIdError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        Self::new(value)
53    }
54}
55
56/// A non-empty robot serial number.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub struct RobotSerialNumber(String);
59
60impl RobotSerialNumber {
61    /// Creates a serial number from non-empty text.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`RobotIdError::Empty`] when the trimmed serial number is empty.
66    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotIdError> {
67        non_empty_identifier(value).map(Self)
68    }
69
70    /// Returns the serial number text.
71    #[must_use]
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75
76    /// Consumes the serial number and returns the owned string.
77    #[must_use]
78    pub fn into_string(self) -> String {
79        self.0
80    }
81}
82
83impl AsRef<str> for RobotSerialNumber {
84    fn as_ref(&self) -> &str {
85        self.as_str()
86    }
87}
88
89impl fmt::Display for RobotSerialNumber {
90    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        formatter.write_str(self.as_str())
92    }
93}
94
95impl FromStr for RobotSerialNumber {
96    type Err = RobotIdError;
97
98    fn from_str(value: &str) -> Result<Self, Self::Err> {
99        Self::new(value)
100    }
101}
102
103/// A non-empty robot instance identifier.
104#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
105pub struct RobotInstanceId(String);
106
107impl RobotInstanceId {
108    /// Creates an instance ID from non-empty text.
109    ///
110    /// # Errors
111    ///
112    /// Returns [`RobotIdError::Empty`] when the trimmed instance ID is empty.
113    pub fn new(value: impl AsRef<str>) -> Result<Self, RobotIdError> {
114        non_empty_identifier(value).map(Self)
115    }
116
117    /// Returns the instance ID text.
118    #[must_use]
119    pub fn as_str(&self) -> &str {
120        &self.0
121    }
122
123    /// Consumes the instance ID and returns the owned string.
124    #[must_use]
125    pub fn into_string(self) -> String {
126        self.0
127    }
128}
129
130impl AsRef<str> for RobotInstanceId {
131    fn as_ref(&self) -> &str {
132        self.as_str()
133    }
134}
135
136impl fmt::Display for RobotInstanceId {
137    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138        formatter.write_str(self.as_str())
139    }
140}
141
142impl FromStr for RobotInstanceId {
143    type Err = RobotIdError;
144
145    fn from_str(value: &str) -> Result<Self, Self::Err> {
146        Self::new(value)
147    }
148}
149
150/// Errors returned while constructing robot identity values.
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub enum RobotIdError {
153    /// The identifier was empty after trimming whitespace.
154    Empty,
155}
156
157impl fmt::Display for RobotIdError {
158    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            Self::Empty => formatter.write_str("robot identifier cannot be empty"),
161        }
162    }
163}
164
165impl Error for RobotIdError {}
166
167fn non_empty_identifier(value: impl AsRef<str>) -> Result<String, RobotIdError> {
168    let trimmed = value.as_ref().trim();
169
170    if trimmed.is_empty() {
171        Err(RobotIdError::Empty)
172    } else {
173        Ok(trimmed.to_string())
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::{RobotId, RobotIdError, RobotInstanceId, RobotSerialNumber};
180
181    #[test]
182    fn constructs_valid_robot_id() -> Result<(), RobotIdError> {
183        let id = RobotId::new("robot:A-17")?;
184
185        assert_eq!(id.as_str(), "robot:A-17");
186        Ok(())
187    }
188
189    #[test]
190    fn rejects_empty_robot_id() {
191        assert_eq!(RobotId::new("  "), Err(RobotIdError::Empty));
192    }
193
194    #[test]
195    fn constructs_serial_number() -> Result<(), RobotIdError> {
196        let serial = RobotSerialNumber::new("SN-2026-A")?;
197
198        assert_eq!(serial.as_str(), "SN-2026-A");
199        Ok(())
200    }
201
202    #[test]
203    fn constructs_instance_id() -> Result<(), RobotIdError> {
204        let instance = RobotInstanceId::new("cell-4/arm-1")?;
205
206        assert_eq!(instance.as_str(), "cell-4/arm-1");
207        Ok(())
208    }
209
210    #[test]
211    fn displays_identifiers() -> Result<(), RobotIdError> {
212        let id = RobotId::new("Robot.ID:42")?;
213
214        assert_eq!(id.to_string(), "Robot.ID:42");
215        Ok(())
216    }
217}