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 EndEffectorName(String);
12
13impl EndEffectorName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, EndEffectorTextError> {
20 non_empty_end_effector_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 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum EndEffectorKind {
59 Gripper,
61 VacuumGripper,
63 Welder,
65 Cutter,
67 Drill,
69 Nozzle,
71 SuctionCup,
73 ToolChanger,
75 Unknown,
77 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub enum GripState {
125 Open,
127 Closed,
129 Holding,
131 Released,
133 Unknown,
135 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
174pub struct ToolMount(String);
175
176impl ToolMount {
177 pub fn new(value: impl AsRef<str>) -> Result<Self, EndEffectorTextError> {
183 non_empty_end_effector_text(value).map(Self)
184 }
185
186 #[must_use]
188 pub fn as_str(&self) -> &str {
189 &self.0
190 }
191
192 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum EndEffectorTextError {
222 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
238pub enum EndEffectorKindParseError {
239 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255pub enum GripStateParseError {
256 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}