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 FrameName(String);
12
13impl FrameName {
14 pub fn new(value: impl AsRef<str>) -> Result<Self, FrameTextError> {
20 non_empty_frame_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 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
58pub enum FrameKind {
59 World,
61 Map,
63 Odom,
65 Base,
67 Tool,
69 Sensor,
71 Joint,
73 Link,
75 Unknown,
77 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub struct FrameRef {
125 name: FrameName,
126 kind: FrameKind,
127}
128
129impl FrameRef {
130 #[must_use]
132 pub const fn new(name: FrameName, kind: FrameKind) -> Self {
133 Self { name, kind }
134 }
135
136 #[must_use]
138 pub const fn named(name: FrameName) -> Self {
139 Self {
140 name,
141 kind: FrameKind::Unknown,
142 }
143 }
144
145 #[must_use]
147 pub const fn name(&self) -> &FrameName {
148 &self.name
149 }
150
151 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
166pub struct FrameRelation {
167 parent: FrameRef,
168 child: FrameRef,
169}
170
171impl FrameRelation {
172 #[must_use]
174 pub const fn new(parent: FrameRef, child: FrameRef) -> Self {
175 Self { parent, child }
176 }
177
178 #[must_use]
180 pub const fn parent(&self) -> &FrameRef {
181 &self.parent
182 }
183
184 #[must_use]
186 pub const fn child(&self) -> &FrameRef {
187 &self.child
188 }
189}
190
191#[derive(Clone, Copy, Debug, Eq, PartialEq)]
193pub enum FrameTextError {
194 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub enum FrameKindParseError {
211 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}