ros2_client/
names.rs

1//! This module defines types to represent ROS 2 names for
2//! * Message types, e.g. `std_msgs/String`
3//! * Service types, e.g. `turtlesim/Spawn`
4//! * action types, e.g. `turtlesim/RotateAbsolute`
5//! *
6
7use std::fmt;
8
9// TODO:
10// Conform fully to https://design.ros2.org/articles/topic_and_service_names.html
11// and
12// https://wiki.ros.org/Names --> Section 1.1.1 Valid Names
13
14/// Names for Nodes
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct NodeName {
17  namespace: String,
18  base_name: String,
19}
20
21impl NodeName {
22  pub fn new(namespace: &str, base_name: &str) -> Result<NodeName, NameError> {
23    match base_name.chars().next() {
24      None => return Err(NameError::Empty),
25      Some(c) if c.is_ascii_alphabetic() || c == '_' => { /*ok*/ }
26      Some(other) => return Err(NameError::BadChar(other)),
27    }
28
29    if let Some(bad) = base_name
30      .chars()
31      .find(|c| !(c.is_ascii_alphanumeric() || *c == '_'))
32    {
33      return Err(NameError::BadChar(bad));
34    }
35
36    match namespace.chars().next() {
37      None => {
38        return Err(NameError::BadSlash(
39          "<empty_namespace>".to_owned(),
40          base_name.to_owned(),
41        ))
42      }
43      Some(c) if c.is_ascii_alphabetic() || c == '/' => { /*ok*/ }
44      // Character '~' is not accepted, because we do not know what that would mean in a Node's
45      // name.
46      Some(other) => return Err(NameError::BadChar(other)),
47    }
48
49    if namespace.starts_with('/') {
50      // This is what we expect
51    } else {
52      return Err(NameError::BadSlash(
53        namespace.to_owned(),
54        base_name.to_owned(),
55      ));
56    }
57
58    // Otherwise, what would be the absolute node name?
59    if let Some(bad) = namespace
60      .chars()
61      .find(|c| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '/'))
62    {
63      return Err(NameError::BadChar(bad));
64    }
65
66    if namespace.ends_with('/') && namespace != "/" {
67      return Err(NameError::BadSlash(
68        namespace.to_owned(),
69        base_name.to_owned(),
70      ));
71    }
72
73    Ok(NodeName {
74      namespace: namespace.to_owned(),
75      base_name: base_name.to_owned(),
76    })
77  }
78
79  pub fn namespace(&self) -> &str {
80    &self.namespace
81  }
82  pub fn base_name(&self) -> &str {
83    &self.base_name
84  }
85
86  pub fn fully_qualified_name(&self) -> String {
87    let mut fqn = self.namespace.clone();
88    if fqn.ends_with('/') {
89      // do nothing, it already ends with slash, i.e. is exactly "/"
90    } else {
91      fqn.push('/');
92    }
93    fqn.push_str(&self.base_name);
94    fqn
95  }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum NameError {
100  Empty,
101  BadChar(char),
102  BadSlash(String, String),
103}
104
105impl fmt::Display for NameError {
106  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107    match self {
108      NameError::Empty => write!(f, "Base name must not be empty"),
109      NameError::BadChar(c) => write!(f, "Bad chracters in Name: {c:?}"),
110      NameError::BadSlash(ns, n) => write!(
111        f,
112        "Invalid placement of seprator slashes. namespace={ns}  name={n}"
113      ),
114    }
115  }
116}
117
118impl std::error::Error for NameError {}
119
120/// Names for Topics, Services
121///
122/// See [Names](https://wiki.ros.org/Names) for ROS 1.
123/// and [topic and Service name mapping to DDS](https://design.ros2.org/articles/topic_and_service_names.html)
124/// in ROS 2 documentation.
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct Name {
127  base_name: String, // The last part of the full name. Must not be empty.
128  preceeding_tokens: Vec<String>, // without separating slashes
129  absolute: bool,    // in string format, absolute names begin with a slash
130}
131
132// TODO: We do not (yet) support tilde-expansion or brace-substitutions.
133
134impl Name {
135  /// Construct a new `Name` from namespace and base name.
136  ///
137  /// If the namespace begins with a slash (`/`) character, the Name will be
138  /// absolute, otherwise it will be relative.
139  /// The namespace may consist of several components, separated by slashes.
140  /// Tha namespace must not end in a slash, unless the namespace is just `"/"`.
141  ///
142  /// Do not put slashes in the `base_name`.
143  /// Base name is not allowed to be empty, but the namespace may be empty.
144  ///
145  /// Tilde or brace substitutions are not (yet) supported.
146  pub fn new(namespace: &str, base_name: &str) -> Result<Name, NameError> {
147    // TODO: Implement all of the checks here
148    let (namespace_rel, absolute) = if let Some(rel) = namespace.strip_prefix('/') {
149      (rel, true)
150    } else {
151      (namespace, false)
152    };
153
154    if base_name.is_empty() {
155      return Err(NameError::Empty);
156    }
157
158    let ok_start_char = |c: char| c.is_ascii_alphabetic() || c == '_';
159    let no_multi_underscore = |s: &str| !s.contains("__");
160
161    if let Some(bad) = base_name
162      .chars()
163      .find(|c| !(c.is_ascii_alphanumeric() || *c == '_'))
164    {
165      return Err(NameError::BadChar(bad));
166    } else if !base_name.starts_with(ok_start_char) {
167      return Err(NameError::BadChar(base_name.chars().next().unwrap_or('?')));
168    } else if !no_multi_underscore(base_name) {
169      return Err(NameError::BadChar('_'));
170    } else {
171      // ok
172    }
173
174    let preceeding_tokens = if namespace_rel.is_empty() {
175      // If the namespace is "" or "/", we want [] instead of [""]
176      Vec::new()
177    } else {
178      namespace_rel
179        .split('/')
180        .map(str::to_owned)
181        .collect::<Vec<String>>()
182      // Starting slash, ending slash, or repeated slash all
183      // produce empty strings.
184    };
185
186    if preceeding_tokens.iter().any(String::is_empty) {
187      return Err(NameError::BadSlash(
188        namespace_rel.to_owned(),
189        base_name.to_owned(),
190      ));
191    }
192
193    if preceeding_tokens.iter().all(|tok| {
194      tok.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
195        && tok.starts_with(ok_start_char)
196        && no_multi_underscore(tok)
197    }) { /* ok */
198    } else {
199      return Err(NameError::BadChar('?')); //TODO. Find which char is bad.
200    }
201
202    Ok(Name {
203      base_name: base_name.to_owned(),
204      preceeding_tokens,
205      absolute,
206    })
207  }
208
209  /// Construct a new `Name` from slash-separated namespace and base name.
210  ///
211  /// e.g. `myspace/some_name`
212  pub fn parse(full_name: &str) -> Result<Name, NameError> {
213    match full_name.rsplit_once('/') {
214      // no slash, just a base name, so namespace is "".
215      None => Name::new("", full_name),
216
217      // Just a single slash, i.e. empty namespace and empty base name.
218      // Not acceptable.
219      Some(("", "")) => Err(NameError::Empty),
220
221      // Last character was slash => base name is empty => bad.
222      Some((bad, "")) => Err(NameError::BadSlash(bad.to_owned(), "".to_owned())),
223
224      // Input was "/foobar", so name is absolute
225      Some(("", base)) => Name::new("/", base),
226
227      // General case: <nonempty> "/" <base_name>
228      Some((prefix, base)) => {
229        if prefix.ends_with('/') {
230          // There was a double slash => Bad.
231          Err(NameError::BadSlash(prefix.to_owned(), base.to_owned()))
232        } else {
233          Name::new(prefix, base)
234        }
235      }
236    }
237  }
238
239  pub fn to_dds_name(&self, kind_prefix: &str, node: &NodeName, suffix: &str) -> String {
240    let mut result = kind_prefix.to_owned();
241    assert!(!result.ends_with('/')); // "rt"
242    if self.absolute {
243      // absolute name: do not add node namespace
244    } else {
245      // relative name: Prefix with Node namespace
246      result.push_str(node.namespace()); // "rt/node_ns"
247    }
248    result.push('/'); // "rt/node_ns/" or "rt/"
249    self.preceeding_tokens.iter().for_each(|tok| {
250      result.push_str(tok);
251      result.push('/');
252    });
253    // rt/node_ns/prec_tok1/
254    result.push_str(&self.base_name);
255    result.push_str(suffix);
256    result
257  }
258
259  pub(crate) fn push(&self, new_suffix: &str) -> Name {
260    //TODO: Check that we still satisfy naming rules
261    let mut preceeding_tokens = self.preceeding_tokens.clone();
262    preceeding_tokens.push(self.base_name.to_string());
263    Name {
264      base_name: new_suffix.to_string(),
265      preceeding_tokens,
266      absolute: self.absolute,
267    }
268  }
269
270  pub fn is_absolute(&self) -> bool {
271    self.absolute
272  }
273}
274
275impl fmt::Display for Name {
276  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
277    if self.absolute {
278      write!(f, "/")?;
279    }
280    for t in &self.preceeding_tokens {
281      write!(f, "{t}/")?;
282    }
283    write!(f, "{}", self.base_name)
284  }
285}
286
287/// Name for `.msg` type, or a data type carried over a Topic.
288///
289/// This would be called a "Pacakge Resource Name", at least in ROS 1.
290///
291/// Note that this is not for naming Topics, but data types of Topics.
292///
293/// See [Names](https://wiki.ros.org/Names) Section 1.2 Package Resource Names.
294#[derive(Clone, Debug)]
295pub struct MessageTypeName {
296  prefix: String, // typically "msg", but may be "action". What should this part be called?
297  //TODO: String is strictly UTF-8, but ROS2 uses just byte strings that are recommended to be
298  // UTF-8
299  ros2_package_name: String, // or should this be "namespace"?
300  ros2_type_name: String,
301}
302
303impl MessageTypeName {
304  pub fn new(package_name: &str, type_name: &str) -> Self {
305    //TODO: Ensure parameters have no leading/trailing slashes
306    MessageTypeName {
307      prefix: "msg".to_string(),
308      ros2_package_name: package_name.to_owned(),
309      ros2_type_name: type_name.to_owned(),
310    }
311  }
312
313  pub(crate) fn new_prefix(package_name: &str, type_name: &str, prefix: String) -> Self {
314    MessageTypeName {
315      prefix,
316      ros2_package_name: package_name.to_owned(),
317      ros2_type_name: type_name.to_owned(),
318    }
319  }
320
321  pub fn package_name(&self) -> &str {
322    self.ros2_package_name.as_str()
323  }
324
325  pub fn type_name(&self) -> &str {
326    self.ros2_type_name.as_str()
327  }
328
329  /// Convert to type name used over DDS
330  pub fn dds_msg_type(&self) -> String {
331    slash_to_colons(
332      self.ros2_package_name.clone() + "/" + &self.prefix + "/dds_/" + &self.ros2_type_name + "_",
333    )
334  }
335}
336
337fn slash_to_colons(s: String) -> String {
338  s.replace('/', "::")
339}
340
341/// Similar to [`MessageTypeName`], but names a Service type.
342#[derive(Clone, Debug)]
343pub struct ServiceTypeName {
344  prefix: String,
345  msg: MessageTypeName,
346}
347
348impl ServiceTypeName {
349  pub fn new(package_name: &str, type_name: &str) -> Self {
350    ServiceTypeName {
351      prefix: "srv".to_string(),
352      msg: MessageTypeName::new(package_name, type_name),
353    }
354  }
355
356  pub(crate) fn new_prefix(package_name: &str, type_name: &str, prefix: String) -> Self {
357    ServiceTypeName {
358      prefix,
359      msg: MessageTypeName::new(package_name, type_name),
360    }
361  }
362
363  pub fn package_name(&self) -> &str {
364    self.msg.package_name()
365  }
366
367  pub fn type_name(&self) -> &str {
368    self.msg.type_name()
369  }
370
371  pub(crate) fn dds_request_type(&self) -> String {
372    slash_to_colons(
373      self.package_name().to_owned()
374        + "/"
375        + &self.prefix
376        + "/dds_/"
377        + self.type_name()
378        + "_Request_",
379    )
380  }
381
382  pub(crate) fn dds_response_type(&self) -> String {
383    slash_to_colons(
384      self.package_name().to_owned()
385        + "/"
386        + &self.prefix
387        + "/dds_/"
388        + self.type_name()
389        + "_Response_",
390    )
391  }
392}
393
394/// Similar to [`MessageTypeName`], but names an Action type.
395#[derive(Clone, Debug)]
396pub struct ActionTypeName(MessageTypeName);
397
398impl ActionTypeName {
399  pub fn new(package_name: &str, type_name: &str) -> Self {
400    ActionTypeName(MessageTypeName::new(package_name, type_name))
401  }
402
403  pub fn package_name(&self) -> &str {
404    self.0.package_name()
405  }
406
407  pub fn type_name(&self) -> &str {
408    self.0.type_name()
409  }
410
411  pub(crate) fn dds_action_topic(&self, topic: &str) -> MessageTypeName {
412    MessageTypeName::new_prefix(
413      self.package_name(),
414      &(self.type_name().to_owned() + topic),
415      "action".to_owned(),
416    )
417    //slash_to_colons(self.package_name().to_owned() + "/action/dds_/" +
418    // &self.type_name())
419  }
420
421  pub(crate) fn dds_action_service(&self, srv: &str) -> ServiceTypeName {
422    ServiceTypeName::new_prefix(
423      self.package_name(),
424      &(self.type_name().to_owned() + srv),
425      "action".to_owned(),
426    )
427  }
428}
429
430// -------------------------------------------------------------------------------------
431// -------------------------------------------------------------------------------------
432
433#[test]
434fn test_name() {
435  assert!(Name::new("", "").is_err());
436  assert!(Name::new("", "/").is_err());
437  assert!(Name::new("a", "b").is_ok());
438  assert!(Name::new("a", "_b").is_ok());
439  assert!(Name::new("a", "b_b").is_ok()); // may contain [...] underscores (_), [...]
440  assert!(Name::new("a", "b__b").is_err()); // must not contain any number of repeated underscores (_)
441  assert!(Name::new("a2//a", "b").is_err()); // must not contain any number of
442                                             // repeated forward slashes (/)
443}
444
445#[test]
446fn test_name_parse() {
447  // https://design.ros2.org/articles/topic_and_service_names.html
448
449  assert!(Name::parse("").is_err()); // must not be empty
450  assert!(Name::parse("/").is_err()); // must not be empty
451  assert!(Name::parse("a/").is_err()); // must not be empty
452  assert!(Name::parse("a/b/").is_err());
453
454  assert!(Name::parse("2").is_err()); // must not start with a numeric character ([0-9])
455  assert!(Name::parse("2/a").is_err()); // must not start with a numeric character ([0-9])
456  assert!(Name::parse("a2/a").is_ok());
457  assert!(Name::parse("_a2/a").is_ok()); // may contain [...] underscores (_), [...]
458  assert!(Name::parse("some_name/a").is_ok()); // may contain [...] underscores (_), [...]
459  assert!(Name::parse("__a2/a").is_err()); // must not contain any number of repeated underscores (_)
460  assert!(Name::parse("a2//a").is_err()); // must not contain any number of repeated forward slashes (/)
461
462  assert_eq!(Name::parse("a/nn").unwrap(), Name::new("a", "nn").unwrap());
463  assert_eq!(
464    Name::parse("a/b/c/nn").unwrap(),
465    Name::new("a/b/c", "nn").unwrap()
466  );
467  assert_eq!(
468    Name::parse("/a/b/c/nn").unwrap(),
469    Name::new("/a/b/c", "nn").unwrap()
470  );
471
472  assert!(!Name::parse("a/nn").unwrap().is_absolute());
473  assert!(Name::parse("/a/nn").unwrap().is_absolute());
474}