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 PoseName(String);
12
13impl PoseName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, PoseTextError> {
20 non_empty_pose_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 PoseName {
37 fn as_ref(&self) -> &str {
38 self.as_str()
39 }
40}
41
42impl fmt::Display for PoseName {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 formatter.write_str(self.as_str())
45 }
46}
47
48impl FromStr for PoseName {
49 type Err = PoseTextError;
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 PoseKind {
59 Home,
61 Ready,
63 Rest,
65 Tool,
67 Target,
69 Waypoint,
71 Calibration,
73 Unknown,
75 Custom(String),
77}
78
79impl fmt::Display for PoseKind {
80 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81 formatter.write_str(match self {
82 Self::Home => "home",
83 Self::Ready => "ready",
84 Self::Rest => "rest",
85 Self::Tool => "tool",
86 Self::Target => "target",
87 Self::Waypoint => "waypoint",
88 Self::Calibration => "calibration",
89 Self::Unknown => "unknown",
90 Self::Custom(value) => value.as_str(),
91 })
92 }
93}
94
95impl FromStr for PoseKind {
96 type Err = PoseKindParseError;
97
98 fn from_str(value: &str) -> Result<Self, Self::Err> {
99 let trimmed = value.trim();
100 if trimmed.is_empty() {
101 return Err(PoseKindParseError::Empty);
102 }
103
104 match normalized_token(trimmed).as_str() {
105 "home" => Ok(Self::Home),
106 "ready" => Ok(Self::Ready),
107 "rest" => Ok(Self::Rest),
108 "tool" => Ok(Self::Tool),
109 "target" => Ok(Self::Target),
110 "waypoint" => Ok(Self::Waypoint),
111 "calibration" => Ok(Self::Calibration),
112 "unknown" => Ok(Self::Unknown),
113 _ => Ok(Self::Custom(trimmed.to_string())),
114 }
115 }
116}
117
118#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
120pub struct Pose2Label(String);
121
122impl Pose2Label {
123 pub fn new(value: impl AsRef<str>) -> Result<Self, PoseTextError> {
129 non_empty_pose_text(value).map(Self)
130 }
131
132 #[must_use]
134 pub fn as_str(&self) -> &str {
135 &self.0
136 }
137
138 #[must_use]
140 pub fn into_string(self) -> String {
141 self.0
142 }
143}
144
145impl AsRef<str> for Pose2Label {
146 fn as_ref(&self) -> &str {
147 self.as_str()
148 }
149}
150
151impl fmt::Display for Pose2Label {
152 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
153 formatter.write_str(self.as_str())
154 }
155}
156
157impl FromStr for Pose2Label {
158 type Err = PoseTextError;
159
160 fn from_str(value: &str) -> Result<Self, Self::Err> {
161 Self::new(value)
162 }
163}
164
165#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct Pose3Label(String);
168
169impl Pose3Label {
170 pub fn new(value: impl AsRef<str>) -> Result<Self, PoseTextError> {
176 non_empty_pose_text(value).map(Self)
177 }
178
179 #[must_use]
181 pub fn as_str(&self) -> &str {
182 &self.0
183 }
184
185 #[must_use]
187 pub fn into_string(self) -> String {
188 self.0
189 }
190}
191
192impl AsRef<str> for Pose3Label {
193 fn as_ref(&self) -> &str {
194 self.as_str()
195 }
196}
197
198impl fmt::Display for Pose3Label {
199 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200 formatter.write_str(self.as_str())
201 }
202}
203
204impl FromStr for Pose3Label {
205 type Err = PoseTextError;
206
207 fn from_str(value: &str) -> Result<Self, Self::Err> {
208 Self::new(value)
209 }
210}
211
212#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
214pub enum OrientationKind {
215 Euler,
217 Quaternion,
219 AxisAngle,
221 RotationMatrix,
223 Unknown,
225 Custom(String),
227}
228
229impl fmt::Display for OrientationKind {
230 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231 formatter.write_str(match self {
232 Self::Euler => "euler",
233 Self::Quaternion => "quaternion",
234 Self::AxisAngle => "axis-angle",
235 Self::RotationMatrix => "rotation-matrix",
236 Self::Unknown => "unknown",
237 Self::Custom(value) => value.as_str(),
238 })
239 }
240}
241
242impl FromStr for OrientationKind {
243 type Err = OrientationKindParseError;
244
245 fn from_str(value: &str) -> Result<Self, Self::Err> {
246 let trimmed = value.trim();
247 if trimmed.is_empty() {
248 return Err(OrientationKindParseError::Empty);
249 }
250
251 match normalized_token(trimmed).as_str() {
252 "euler" => Ok(Self::Euler),
253 "quaternion" => Ok(Self::Quaternion),
254 "axis-angle" => Ok(Self::AxisAngle),
255 "rotation-matrix" => Ok(Self::RotationMatrix),
256 "unknown" => Ok(Self::Unknown),
257 _ => Ok(Self::Custom(trimmed.to_string())),
258 }
259 }
260}
261
262#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub enum PoseTextError {
265 Empty,
267}
268
269impl fmt::Display for PoseTextError {
270 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271 match self {
272 Self::Empty => formatter.write_str("pose text cannot be empty"),
273 }
274 }
275}
276
277impl Error for PoseTextError {}
278
279#[derive(Clone, Copy, Debug, Eq, PartialEq)]
281pub enum PoseKindParseError {
282 Empty,
284}
285
286impl fmt::Display for PoseKindParseError {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 match self {
289 Self::Empty => formatter.write_str("pose kind cannot be empty"),
290 }
291 }
292}
293
294impl Error for PoseKindParseError {}
295
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
298pub enum OrientationKindParseError {
299 Empty,
301}
302
303impl fmt::Display for OrientationKindParseError {
304 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
305 match self {
306 Self::Empty => formatter.write_str("orientation kind cannot be empty"),
307 }
308 }
309}
310
311impl Error for OrientationKindParseError {}
312
313fn non_empty_pose_text(value: impl AsRef<str>) -> Result<String, PoseTextError> {
314 let trimmed = value.as_ref().trim();
315
316 if trimmed.is_empty() {
317 Err(PoseTextError::Empty)
318 } else {
319 Ok(trimmed.to_string())
320 }
321}
322
323fn normalized_token(value: &str) -> String {
324 value
325 .trim()
326 .chars()
327 .map(|character| match character {
328 '_' | ' ' => '-',
329 other => other.to_ascii_lowercase(),
330 })
331 .collect()
332}
333
334#[cfg(test)]
335mod tests {
336 use super::{
337 OrientationKind, OrientationKindParseError, PoseKind, PoseKindParseError, PoseName,
338 PoseTextError,
339 };
340
341 #[test]
342 fn constructs_valid_pose_name() -> Result<(), PoseTextError> {
343 let name = PoseName::new(" home ")?;
344
345 assert_eq!(name.as_str(), "home");
346 Ok(())
347 }
348
349 #[test]
350 fn rejects_empty_pose_name() {
351 assert_eq!(PoseName::new(""), Err(PoseTextError::Empty));
352 }
353
354 #[test]
355 fn displays_and_parses_pose_kind() -> Result<(), PoseKindParseError> {
356 assert_eq!("waypoint".parse::<PoseKind>()?, PoseKind::Waypoint);
357 assert_eq!(PoseKind::Calibration.to_string(), "calibration");
358 Ok(())
359 }
360
361 #[test]
362 fn displays_and_parses_orientation_kind() -> Result<(), OrientationKindParseError> {
363 assert_eq!(
364 "axis angle".parse::<OrientationKind>()?,
365 OrientationKind::AxisAngle
366 );
367 assert_eq!(
368 OrientationKind::RotationMatrix.to_string(),
369 "rotation-matrix"
370 );
371 Ok(())
372 }
373
374 #[test]
375 fn stores_custom_pose_kind() -> Result<(), PoseKindParseError> {
376 assert_eq!(
377 "inspection-hover".parse::<PoseKind>()?,
378 PoseKind::Custom("inspection-hover".to_string())
379 );
380 Ok(())
381 }
382}