Skip to main content

use_frame/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Primitive robotics frame vocabulary.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// A non-empty robotics frame name.
10#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub struct FrameName(String);
12
13impl FrameName {
14    /// Creates a frame name from non-empty text.
15    ///
16    /// # Errors
17    ///
18    /// Returns [`FrameTextError::Empty`] when the trimmed name is empty.
19    pub fn new(value: impl AsRef<str>) -> Result<Self, FrameTextError> {
20        non_empty_frame_text(value).map(Self)
21    }
22
23    /// Returns the frame 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 FrameName {
37    fn as_ref(&self) -> &str {
38        self.as_str()
39    }
40}
41
42impl fmt::Display for FrameName {
43    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44        formatter.write_str(self.as_str())
45    }
46}
47
48impl FromStr for FrameName {
49    type Err = FrameTextError;
50
51    fn from_str(value: &str) -> Result<Self, Self::Err> {
52        Self::new(value)
53    }
54}
55
56/// Descriptive robotics frame kind vocabulary.
57#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum FrameKind {
59    /// World frame label.
60    World,
61    /// Map frame label.
62    Map,
63    /// Odometry frame label.
64    Odom,
65    /// Robot base frame label.
66    Base,
67    /// Tool frame label.
68    Tool,
69    /// Sensor frame label.
70    Sensor,
71    /// Joint frame label.
72    Joint,
73    /// Link frame label.
74    Link,
75    /// Unknown frame kind.
76    Unknown,
77    /// Caller-defined frame kind text.
78    Custom(String),
79}
80
81impl fmt::Display for FrameKind {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        formatter.write_str(match self {
84            Self::World => "world",
85            Self::Map => "map",
86            Self::Odom => "odom",
87            Self::Base => "base",
88            Self::Tool => "tool",
89            Self::Sensor => "sensor",
90            Self::Joint => "joint",
91            Self::Link => "link",
92            Self::Unknown => "unknown",
93            Self::Custom(value) => value.as_str(),
94        })
95    }
96}
97
98impl FromStr for FrameKind {
99    type Err = FrameKindParseError;
100
101    fn from_str(value: &str) -> Result<Self, Self::Err> {
102        let trimmed = value.trim();
103        if trimmed.is_empty() {
104            return Err(FrameKindParseError::Empty);
105        }
106
107        match normalized_token(trimmed).as_str() {
108            "world" => Ok(Self::World),
109            "map" => Ok(Self::Map),
110            "odom" | "odometry" => Ok(Self::Odom),
111            "base" | "base-link" => Ok(Self::Base),
112            "tool" => Ok(Self::Tool),
113            "sensor" => Ok(Self::Sensor),
114            "joint" => Ok(Self::Joint),
115            "link" => Ok(Self::Link),
116            "unknown" => Ok(Self::Unknown),
117            _ => Ok(Self::Custom(trimmed.to_string())),
118        }
119    }
120}
121
122/// A named frame reference with a descriptive kind.
123#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub struct FrameRef {
125    name: FrameName,
126    kind: FrameKind,
127}
128
129impl FrameRef {
130    /// Creates a frame reference from a name and kind.
131    #[must_use]
132    pub const fn new(name: FrameName, kind: FrameKind) -> Self {
133        Self { name, kind }
134    }
135
136    /// Creates a frame reference with an unknown kind.
137    #[must_use]
138    pub const fn named(name: FrameName) -> Self {
139        Self {
140            name,
141            kind: FrameKind::Unknown,
142        }
143    }
144
145    /// Returns the frame name.
146    #[must_use]
147    pub const fn name(&self) -> &FrameName {
148        &self.name
149    }
150
151    /// Returns the frame kind.
152    #[must_use]
153    pub const fn kind(&self) -> &FrameKind {
154        &self.kind
155    }
156}
157
158impl fmt::Display for FrameRef {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        write!(formatter, "{}:{}", self.kind, self.name)
161    }
162}
163
164/// A descriptive parent/child frame relation.
165#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
166pub struct FrameRelation {
167    parent: FrameRef,
168    child: FrameRef,
169}
170
171impl FrameRelation {
172    /// Creates a parent/child frame relation label.
173    #[must_use]
174    pub const fn new(parent: FrameRef, child: FrameRef) -> Self {
175        Self { parent, child }
176    }
177
178    /// Returns the parent frame reference.
179    #[must_use]
180    pub const fn parent(&self) -> &FrameRef {
181        &self.parent
182    }
183
184    /// Returns the child frame reference.
185    #[must_use]
186    pub const fn child(&self) -> &FrameRef {
187        &self.child
188    }
189}
190
191/// Errors returned while constructing frame text values.
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub enum FrameTextError {
194    /// The value was empty after trimming whitespace.
195    Empty,
196}
197
198impl fmt::Display for FrameTextError {
199    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            Self::Empty => formatter.write_str("frame text cannot be empty"),
202        }
203    }
204}
205
206impl Error for FrameTextError {}
207
208/// Error returned when parsing frame kinds fails.
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub enum FrameKindParseError {
211    /// The frame kind was empty after trimming whitespace.
212    Empty,
213}
214
215impl fmt::Display for FrameKindParseError {
216    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
217        match self {
218            Self::Empty => formatter.write_str("frame kind cannot be empty"),
219        }
220    }
221}
222
223impl Error for FrameKindParseError {}
224
225fn non_empty_frame_text(value: impl AsRef<str>) -> Result<String, FrameTextError> {
226    let trimmed = value.as_ref().trim();
227
228    if trimmed.is_empty() {
229        Err(FrameTextError::Empty)
230    } else {
231        Ok(trimmed.to_string())
232    }
233}
234
235fn normalized_token(value: &str) -> String {
236    value
237        .trim()
238        .chars()
239        .map(|character| match character {
240            '_' | ' ' => '-',
241            other => other.to_ascii_lowercase(),
242        })
243        .collect()
244}
245
246#[cfg(test)]
247mod tests {
248    use super::{
249        FrameKind, FrameKindParseError, FrameName, FrameRef, FrameRelation, FrameTextError,
250    };
251
252    #[test]
253    fn constructs_valid_frame_name() -> Result<(), FrameTextError> {
254        let name = FrameName::new("  base_link  ")?;
255
256        assert_eq!(name.as_str(), "base_link");
257        Ok(())
258    }
259
260    #[test]
261    fn rejects_empty_frame_name() {
262        assert_eq!(FrameName::new(""), Err(FrameTextError::Empty));
263    }
264
265    #[test]
266    fn displays_and_parses_frame_kind() -> Result<(), FrameKindParseError> {
267        assert_eq!("base link".parse::<FrameKind>()?, FrameKind::Base);
268        assert_eq!(FrameKind::Odom.to_string(), "odom");
269        Ok(())
270    }
271
272    #[test]
273    fn stores_custom_frame_kind() -> Result<(), FrameKindParseError> {
274        assert_eq!(
275            "fixture".parse::<FrameKind>()?,
276            FrameKind::Custom("fixture".to_string())
277        );
278        Ok(())
279    }
280
281    #[test]
282    fn constructs_parent_child_relation() -> Result<(), FrameTextError> {
283        let parent = FrameRef::new(FrameName::new("base_link")?, FrameKind::Base);
284        let child = FrameRef::new(FrameName::new("tool0")?, FrameKind::Tool);
285        let relation = FrameRelation::new(parent, child);
286
287        assert_eq!(relation.parent().name().as_str(), "base_link");
288        assert_eq!(relation.child().kind(), &FrameKind::Tool);
289        Ok(())
290    }
291}