Skip to main content

use_end_effector/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive end-effector vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty end-effector name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct EndEffectorName(String);
12
13impl EndEffectorName {
14    /// Creates an end-effector name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`EndEffectorTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, EndEffectorTextError> {
20        non_empty_end_effector_text(value).map(Self)
21    }
22
23    /// Returns the end-effector 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 EndEffectorName {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42impl fmt::Display for EndEffectorName {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for EndEffectorName {
49    type Err = EndEffectorTextError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        Self::new(value)
53    }
54}
55
56/// Descriptive end-effector kind vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum EndEffectorKind {
59    /// Gripper end effector.
60    Gripper,
61    /// Vacuum gripper end effector.
62    VacuumGripper,
63    /// Welder tool.
64    Welder,
65    /// Cutter tool.
66    Cutter,
67    /// Drill tool.
68    Drill,
69    /// Nozzle tool.
70    Nozzle,
71    /// Suction cup end effector.
72    SuctionCup,
73    /// Tool changer end effector.
74    ToolChanger,
75    /// Unknown end-effector kind.
76    Unknown,
77    /// Caller-defined end-effector kind text.
78    Custom(String),
79}
80
81impl fmt::Display for EndEffectorKind {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(match self {
84            Self::Gripper => "gripper",
85            Self::VacuumGripper => "vacuum-gripper",
86            Self::Welder => "welder",
87            Self::Cutter => "cutter",
88            Self::Drill => "drill",
89            Self::Nozzle => "nozzle",
90            Self::SuctionCup => "suction-cup",
91            Self::ToolChanger => "tool-changer",
92            Self::Unknown => "unknown",
93            Self::Custom(value) => value.as_str(),
94        })
95    }
96}
97
98impl FromStr for EndEffectorKind {
99    type Err = EndEffectorKindParseError;
100
101    fn from_str(value: &str) -> Result<Self, Self::Err> {
102        let trimmed = value.trim();
103        if trimmed.is_empty() {
104            return Err(EndEffectorKindParseError::Empty);
105        }
106
107        match normalized_token(trimmed).as_str() {
108            "gripper" => Ok(Self::Gripper),
109            "vacuum-gripper" => Ok(Self::VacuumGripper),
110            "welder" => Ok(Self::Welder),
111            "cutter" => Ok(Self::Cutter),
112            "drill" => Ok(Self::Drill),
113            "nozzle" => Ok(Self::Nozzle),
114            "suction-cup" => Ok(Self::SuctionCup),
115            "tool-changer" => Ok(Self::ToolChanger),
116            "unknown" => Ok(Self::Unknown),
117            _ => Ok(Self::Custom(trimmed.to_string())),
118        }
119    }
120}
121
122/// Descriptive grip state vocabulary.
123#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub enum GripState {
125    /// Open grip state.
126    Open,
127    /// Closed grip state.
128    Closed,
129    /// Holding grip state.
130    Holding,
131    /// Released grip state.
132    Released,
133    /// Unknown grip state.
134    Unknown,
135    /// Caller-defined grip state text.
136    Custom(String),
137}
138
139impl fmt::Display for GripState {
140    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141        formatter.write_str(match self {
142            Self::Open => "open",
143            Self::Closed => "closed",
144            Self::Holding => "holding",
145            Self::Released => "released",
146            Self::Unknown => "unknown",
147            Self::Custom(value) => value.as_str(),
148        })
149    }
150}
151
152impl FromStr for GripState {
153    type Err = GripStateParseError;
154
155    fn from_str(value: &str) -> Result<Self, Self::Err> {
156        let trimmed = value.trim();
157        if trimmed.is_empty() {
158            return Err(GripStateParseError::Empty);
159        }
160
161        match normalized_token(trimmed).as_str() {
162            "open" => Ok(Self::Open),
163            "closed" => Ok(Self::Closed),
164            "holding" => Ok(Self::Holding),
165            "released" | "release" => Ok(Self::Released),
166            "unknown" => Ok(Self::Unknown),
167            _ => Ok(Self::Custom(trimmed.to_string())),
168        }
169    }
170}
171
172/// A non-empty descriptive tool mount label.
173#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
174pub struct ToolMount(String);
175
176impl ToolMount {
177    /// Creates a tool mount label from non-empty text.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`EndEffectorTextError::Empty`] when the trimmed label is empty.
182    pub fn new(value: impl AsRef<str>) -> Result<Self, EndEffectorTextError> {
183        non_empty_end_effector_text(value).map(Self)
184    }
185
186    /// Returns the mount label text.
187    #[must_use]
188    pub fn as_str(&self) -> &str {
189        &self.0
190    }
191
192    /// Consumes the mount label and returns the owned string.
193    #[must_use]
194    pub fn into_string(self) -> String {
195        self.0
196    }
197}
198
199impl AsRef<str> for ToolMount {
200    fn as_ref(&self) -> &str {
201        self.as_str()
202    }
203}
204
205impl fmt::Display for ToolMount {
206    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207        formatter.write_str(self.as_str())
208    }
209}
210
211impl FromStr for ToolMount {
212    type Err = EndEffectorTextError;
213
214    fn from_str(value: &str) -> Result<Self, Self::Err> {
215        Self::new(value)
216    }
217}
218
219/// Errors returned while constructing end-effector text values.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum EndEffectorTextError {
222    /// The value was empty after trimming whitespace.
223    Empty,
224}
225
226impl fmt::Display for EndEffectorTextError {
227    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228        match self {
229            Self::Empty => formatter.write_str("end-effector text cannot be empty"),
230        }
231    }
232}
233
234impl Error for EndEffectorTextError {}
235
236/// Error returned when parsing end-effector kinds fails.
237#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238pub enum EndEffectorKindParseError {
239    /// The end-effector kind was empty after trimming whitespace.
240    Empty,
241}
242
243impl fmt::Display for EndEffectorKindParseError {
244    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
245        match self {
246            Self::Empty => formatter.write_str("end-effector kind cannot be empty"),
247        }
248    }
249}
250
251impl Error for EndEffectorKindParseError {}
252
253/// Error returned when parsing grip states fails.
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255pub enum GripStateParseError {
256    /// The grip state was empty after trimming whitespace.
257    Empty,
258}
259
260impl fmt::Display for GripStateParseError {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        match self {
263            Self::Empty => formatter.write_str("grip state cannot be empty"),
264        }
265    }
266}
267
268impl Error for GripStateParseError {}
269
270fn non_empty_end_effector_text(value: impl AsRef<str>) -> Result<String, EndEffectorTextError> {
271    let trimmed = value.as_ref().trim();
272
273    if trimmed.is_empty() {
274        Err(EndEffectorTextError::Empty)
275    } else {
276        Ok(trimmed.to_string())
277    }
278}
279
280fn normalized_token(value: &str) -> String {
281    value
282        .trim()
283        .chars()
284        .map(|character| match character {
285            '_' | ' ' => '-',
286            other => other.to_ascii_lowercase(),
287        })
288        .collect()
289}
290
291#[cfg(test)]
292mod tests {
293    use super::{
294        EndEffectorKind, EndEffectorKindParseError, EndEffectorName, EndEffectorTextError,
295        GripState, GripStateParseError,
296    };
297
298    #[test]
299    fn constructs_valid_end_effector_name() -> Result<(), EndEffectorTextError> {
300        let name = EndEffectorName::new("  parallel-gripper  ")?;
301
302        assert_eq!(name.as_str(), "parallel-gripper");
303        Ok(())
304    }
305
306    #[test]
307    fn rejects_empty_end_effector_name() {
308        assert_eq!(EndEffectorName::new(""), Err(EndEffectorTextError::Empty));
309    }
310
311    #[test]
312    fn displays_and_parses_end_effector_kind() -> Result<(), EndEffectorKindParseError> {
313        assert_eq!(
314            "vacuum gripper".parse::<EndEffectorKind>()?,
315            EndEffectorKind::VacuumGripper
316        );
317        assert_eq!(EndEffectorKind::ToolChanger.to_string(), "tool-changer");
318        Ok(())
319    }
320
321    #[test]
322    fn displays_and_parses_grip_state() -> Result<(), GripStateParseError> {
323        assert_eq!("open".parse::<GripState>()?, GripState::Open);
324        assert_eq!(GripState::Holding.to_string(), "holding");
325        Ok(())
326    }
327
328    #[test]
329    fn stores_custom_end_effector_kind() -> Result<(), EndEffectorKindParseError> {
330        assert_eq!(
331            "magnetic-gripper".parse::<EndEffectorKind>()?,
332            EndEffectorKind::Custom("magnetic-gripper".to_string())
333        );
334        Ok(())
335    }
336}