1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, num::NonZeroUsize, str::FromStr};
7use std::error::Error;
8
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum KinematicsKind {
12 Forward,
14 Inverse,
16 Differential,
18 Velocity,
20 Position,
22 Unknown,
24 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub struct KinematicChainName(String);
66
67impl KinematicChainName {
68 pub fn new(value: impl AsRef<str>) -> Result<Self, KinematicsTextError> {
74 non_empty_kinematics_text(value).map(Self)
75 }
76
77 #[must_use]
79 pub fn as_str(&self) -> &str {
80 &self.0
81 }
82
83 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub struct DegreeOfFreedom(NonZeroUsize);
113
114impl DegreeOfFreedom {
115 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
150pub struct LinkName(String);
151
152impl LinkName {
153 pub fn new(value: impl AsRef<str>) -> Result<Self, KinematicsTextError> {
159 non_empty_kinematics_text(value).map(Self)
160 }
161
162 #[must_use]
164 pub fn as_str(&self) -> &str {
165 &self.0
166 }
167
168 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub enum KinematicsKindParseError {
198 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub enum KinematicsTextError {
215 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231pub enum DegreeOfFreedomError {
232 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}