Skip to main content

use_pose/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive robot pose vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty pose name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct PoseName(String);
12
13impl PoseName {
14    /// Creates a pose name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`PoseTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, PoseTextError> {
20        non_empty_pose_text(value).map(Self)
21    }
22
23    /// Returns the pose 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 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/// Descriptive pose kind vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum PoseKind {
59    /// Home pose label.
60    Home,
61    /// Ready pose label.
62    Ready,
63    /// Rest pose label.
64    Rest,
65    /// Tool pose label.
66    Tool,
67    /// Target pose label.
68    Target,
69    /// Waypoint pose label.
70    Waypoint,
71    /// Calibration pose label.
72    Calibration,
73    /// Unknown pose kind.
74    Unknown,
75    /// Caller-defined pose kind text.
76    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/// A non-empty 2D pose label.
119#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
120pub struct Pose2Label(String);
121
122impl Pose2Label {
123    /// Creates a 2D pose label from non-empty text.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`PoseTextError::Empty`] when the trimmed label is empty.
128    pub fn new(value: impl AsRef<str>) -> Result<Self, PoseTextError> {
129        non_empty_pose_text(value).map(Self)
130    }
131
132    /// Returns the pose label text.
133    #[must_use]
134    pub fn as_str(&self) -> &str {
135        &self.0
136    }
137
138    /// Consumes the label and returns the owned string.
139    #[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/// A non-empty 3D pose label.
166#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167pub struct Pose3Label(String);
168
169impl Pose3Label {
170    /// Creates a 3D pose label from non-empty text.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`PoseTextError::Empty`] when the trimmed label is empty.
175    pub fn new(value: impl AsRef<str>) -> Result<Self, PoseTextError> {
176        non_empty_pose_text(value).map(Self)
177    }
178
179    /// Returns the pose label text.
180    #[must_use]
181    pub fn as_str(&self) -> &str {
182        &self.0
183    }
184
185    /// Consumes the label and returns the owned string.
186    #[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/// Descriptive orientation representation labels.
213#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
214pub enum OrientationKind {
215    /// Euler orientation label.
216    Euler,
217    /// Quaternion orientation label.
218    Quaternion,
219    /// Axis-angle orientation label.
220    AxisAngle,
221    /// Rotation-matrix orientation label.
222    RotationMatrix,
223    /// Unknown orientation kind.
224    Unknown,
225    /// Caller-defined orientation kind text.
226    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/// Errors returned while constructing pose text values.
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub enum PoseTextError {
265    /// The value was empty after trimming whitespace.
266    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/// Error returned when parsing pose kinds fails.
280#[derive(Clone, Copy, Debug, Eq, PartialEq)]
281pub enum PoseKindParseError {
282    /// The pose kind was empty after trimming whitespace.
283    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/// Error returned when parsing orientation kinds fails.
297#[derive(Clone, Copy, Debug, Eq, PartialEq)]
298pub enum OrientationKindParseError {
299    /// The orientation kind was empty after trimming whitespace.
300    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}