Skip to main content

use_kinematics_label/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive kinematics terminology labels.
5
6use core::{fmt, num::NonZeroUsize, str::FromStr};
7use std::error::Error;
8
9/// Descriptive kinematics terminology.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum KinematicsKind {
12    /// Forward kinematics label.
13    Forward,
14    /// Inverse kinematics label.
15    Inverse,
16    /// Differential kinematics label.
17    Differential,
18    /// Velocity kinematics label.
19    Velocity,
20    /// Position kinematics label.
21    Position,
22    /// Unknown kinematics kind.
23    Unknown,
24    /// Caller-defined kinematics kind text.
25    Custom(String),
26}
27
28impl fmt::Display for KinematicsKind {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        formatter.write_str(match self {
31            Self::Forward => "forward",
32            Self::Inverse => "inverse",
33            Self::Differential => "differential",
34            Self::Velocity => "velocity",
35            Self::Position => "position",
36            Self::Unknown => "unknown",
37            Self::Custom(value) => value.as_str(),
38        })
39    }
40}
41
42impl FromStr for KinematicsKind {
43    type Err = KinematicsKindParseError;
44
45    fn from_str(value: &str) -> Result<Self, Self::Err> {
46        let trimmed = value.trim();
47        if trimmed.is_empty() {
48            return Err(KinematicsKindParseError::Empty);
49        }
50
51        match normalized_token(trimmed).as_str() {
52            "forward" | "forward-kinematics" => Ok(Self::Forward),
53            "inverse" | "inverse-kinematics" => Ok(Self::Inverse),
54            "differential" | "differential-kinematics" => Ok(Self::Differential),
55            "velocity" | "velocity-kinematics" => Ok(Self::Velocity),
56            "position" | "position-kinematics" => Ok(Self::Position),
57            "unknown" => Ok(Self::Unknown),
58            _ => Ok(Self::Custom(trimmed.to_string())),
59        }
60    }
61}
62
63/// A non-empty kinematic chain name.
64#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub struct KinematicChainName(String);
66
67impl KinematicChainName {
68    /// Creates a kinematic chain name from non-empty text.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`KinematicsTextError::Empty`] when the trimmed name is empty.
73    pub fn new(value: impl AsRef<str>) -> Result<Self, KinematicsTextError> {
74        non_empty_kinematics_text(value).map(Self)
75    }
76
77    /// Returns the chain name text.
78    #[must_use]
79    pub fn as_str(&self) -> &str {
80        &self.0
81    }
82
83    /// Consumes the name and returns the owned string.
84    #[must_use]
85    pub fn into_string(self) -> String {
86        self.0
87    }
88}
89
90impl AsRef<str> for KinematicChainName {
91    fn as_ref(&self) -> &str {
92        self.as_str()
93    }
94}
95
96impl fmt::Display for KinematicChainName {
97    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98        formatter.write_str(self.as_str())
99    }
100}
101
102impl FromStr for KinematicChainName {
103    type Err = KinematicsTextError;
104
105    fn from_str(value: &str) -> Result<Self, Self::Err> {
106        Self::new(value)
107    }
108}
109
110/// A non-zero degree-of-freedom count.
111#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub struct DegreeOfFreedom(NonZeroUsize);
113
114impl DegreeOfFreedom {
115    /// Creates a non-zero degree-of-freedom count.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`DegreeOfFreedomError::Zero`] when `value` is zero.
120    pub const fn new(value: usize) -> Result<Self, DegreeOfFreedomError> {
121        match NonZeroUsize::new(value) {
122            Some(value) => Ok(Self(value)),
123            None => Err(DegreeOfFreedomError::Zero),
124        }
125    }
126
127    /// Returns the degree-of-freedom count.
128    #[must_use]
129    pub const fn get(self) -> usize {
130        self.0.get()
131    }
132}
133
134impl TryFrom<usize> for DegreeOfFreedom {
135    type Error = DegreeOfFreedomError;
136
137    fn try_from(value: usize) -> Result<Self, Self::Error> {
138        Self::new(value)
139    }
140}
141
142impl fmt::Display for DegreeOfFreedom {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        self.get().fmt(formatter)
145    }
146}
147
148/// A non-empty link name.
149#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
150pub struct LinkName(String);
151
152impl LinkName {
153    /// Creates a link name from non-empty text.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`KinematicsTextError::Empty`] when the trimmed link name is empty.
158    pub fn new(value: impl AsRef<str>) -> Result<Self, KinematicsTextError> {
159        non_empty_kinematics_text(value).map(Self)
160    }
161
162    /// Returns the link name text.
163    #[must_use]
164    pub fn as_str(&self) -> &str {
165        &self.0
166    }
167
168    /// Consumes the link name and returns the owned string.
169    #[must_use]
170    pub fn into_string(self) -> String {
171        self.0
172    }
173}
174
175impl AsRef<str> for LinkName {
176    fn as_ref(&self) -> &str {
177        self.as_str()
178    }
179}
180
181impl fmt::Display for LinkName {
182    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
183        formatter.write_str(self.as_str())
184    }
185}
186
187impl FromStr for LinkName {
188    type Err = KinematicsTextError;
189
190    fn from_str(value: &str) -> Result<Self, Self::Err> {
191        Self::new(value)
192    }
193}
194
195/// Error returned when parsing kinematics kinds fails.
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub enum KinematicsKindParseError {
198    /// The kinematics kind was empty after trimming whitespace.
199    Empty,
200}
201
202impl fmt::Display for KinematicsKindParseError {
203    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            Self::Empty => formatter.write_str("kinematics kind cannot be empty"),
206        }
207    }
208}
209
210impl Error for KinematicsKindParseError {}
211
212/// Errors returned while constructing kinematics text values.
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub enum KinematicsTextError {
215    /// The value was empty after trimming whitespace.
216    Empty,
217}
218
219impl fmt::Display for KinematicsTextError {
220    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self {
222            Self::Empty => formatter.write_str("kinematics text cannot be empty"),
223        }
224    }
225}
226
227impl Error for KinematicsTextError {}
228
229/// Errors returned while constructing degrees of freedom.
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231pub enum DegreeOfFreedomError {
232    /// Degree-of-freedom counts must be non-zero.
233    Zero,
234}
235
236impl fmt::Display for DegreeOfFreedomError {
237    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238        match self {
239            Self::Zero => formatter.write_str("degree of freedom must be non-zero"),
240        }
241    }
242}
243
244impl Error for DegreeOfFreedomError {}
245
246fn non_empty_kinematics_text(value: impl AsRef<str>) -> Result<String, KinematicsTextError> {
247    let trimmed = value.as_ref().trim();
248
249    if trimmed.is_empty() {
250        Err(KinematicsTextError::Empty)
251    } else {
252        Ok(trimmed.to_string())
253    }
254}
255
256fn normalized_token(value: &str) -> String {
257    value
258        .trim()
259        .chars()
260        .map(|character| match character {
261            '_' | ' ' => '-',
262            other => other.to_ascii_lowercase(),
263        })
264        .collect()
265}
266
267#[cfg(test)]
268mod tests {
269    use super::{
270        DegreeOfFreedom, DegreeOfFreedomError, KinematicChainName, KinematicsKind,
271        KinematicsKindParseError, KinematicsTextError,
272    };
273
274    #[test]
275    fn displays_and_parses_kinematics_kind() -> Result<(), KinematicsKindParseError> {
276        assert_eq!(
277            "forward kinematics".parse::<KinematicsKind>()?,
278            KinematicsKind::Forward
279        );
280        assert_eq!(KinematicsKind::Differential.to_string(), "differential");
281        Ok(())
282    }
283
284    #[test]
285    fn stores_custom_kinematics_kind() -> Result<(), KinematicsKindParseError> {
286        assert_eq!(
287            "redundancy-resolution".parse::<KinematicsKind>()?,
288            KinematicsKind::Custom("redundancy-resolution".to_string())
289        );
290        Ok(())
291    }
292
293    #[test]
294    fn constructs_valid_chain_name() -> Result<(), KinematicsTextError> {
295        let name = KinematicChainName::new("  arm-chain  ")?;
296
297        assert_eq!(name.as_str(), "arm-chain");
298        Ok(())
299    }
300
301    #[test]
302    fn rejects_empty_chain_name() {
303        assert_eq!(KinematicChainName::new(""), Err(KinematicsTextError::Empty));
304    }
305
306    #[test]
307    fn constructs_valid_degree_of_freedom() -> Result<(), DegreeOfFreedomError> {
308        let dof = DegreeOfFreedom::new(6)?;
309
310        assert_eq!(dof.get(), 6);
311        assert_eq!(dof.to_string(), "6");
312        Ok(())
313    }
314
315    #[test]
316    fn rejects_zero_degree_of_freedom() {
317        assert_eq!(DegreeOfFreedom::new(0), Err(DegreeOfFreedomError::Zero));
318    }
319}