1use crate::errors::{Ros2ArgsError, Ros2ArgsResult};
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum NameKind {
65 Topic,
67 Node,
69 Namespace,
71 Substitution,
73}
74
75impl std::fmt::Display for NameKind {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::Topic => write!(f, "topic"),
79 Self::Node => write!(f, "node"),
80 Self::Namespace => write!(f, "namespace"),
81 Self::Substitution => write!(f, "substitution"),
82 }
83 }
84}
85
86#[inline]
90#[must_use]
91pub fn is_valid_name_char(c: char) -> bool {
92 c.is_ascii_alphanumeric() || c == '_'
93}
94
95#[inline]
97#[must_use]
98pub fn is_valid_topic_char(c: char) -> bool {
99 is_valid_name_char(c) || c == '/'
100}
101
102pub fn validate_topic_name(name: &str) -> Ros2ArgsResult<()> {
135 validate_name_impl(name, NameKind::Topic)
136}
137
138pub fn validate_node_name(name: &str) -> Ros2ArgsResult<()> {
165 validate_name_impl(name, NameKind::Node)
166}
167
168pub fn validate_namespace(namespace: &str) -> Ros2ArgsResult<()> {
197 validate_name_impl(namespace, NameKind::Namespace)
198}
199
200pub fn validate_substitution(name: &str) -> Ros2ArgsResult<()> {
223 validate_name_impl(name, NameKind::Substitution)
224}
225
226#[allow(clippy::too_many_lines)]
228fn validate_name_impl(name: &str, kind: NameKind) -> Ros2ArgsResult<()> {
229 if name.is_empty() {
231 return Err(Ros2ArgsError::InvalidName {
232 kind,
233 name: name.to_string(),
234 reason: "name must not be empty".to_string(),
235 });
236 }
237
238 let chars: Vec<char> = name.chars().collect();
239 let mut i = 0;
240
241 match kind {
243 NameKind::Topic => {
244 if chars[0] == '~' {
246 if chars.len() > 1 && chars[1] != '/' {
248 return Err(Ros2ArgsError::InvalidName {
249 kind,
250 name: name.to_string(),
251 reason: "tilde (~) must be followed by a forward slash (/)".to_string(),
252 });
253 }
254 i = 1;
255 } else if chars[0] == '/' {
256 i = 1;
257 } else if chars[0] == '{' {
258 } else if chars[0].is_ascii_digit() {
260 return Err(Ros2ArgsError::InvalidName {
261 kind,
262 name: name.to_string(),
263 reason: "name must not start with a numeric character".to_string(),
264 });
265 } else if !chars[0].is_ascii_alphabetic() && chars[0] != '_' {
266 return Err(Ros2ArgsError::InvalidName {
267 kind,
268 name: name.to_string(),
269 reason: format!("invalid character '{}' at position 0", chars[0]),
270 });
271 }
272 }
273 NameKind::Namespace => {
274 if chars[0] != '/' {
276 return Err(Ros2ArgsError::InvalidName {
277 kind,
278 name: name.to_string(),
279 reason: "namespace must start with a forward slash (/)".to_string(),
280 });
281 }
282 if name == "/" {
284 return Ok(());
285 }
286 i = 1;
287 if i < chars.len() && chars[i].is_ascii_digit() {
289 return Err(Ros2ArgsError::InvalidName {
290 kind,
291 name: name.to_string(),
292 reason: "namespace token must not start with a numeric character".to_string(),
293 });
294 }
295 }
296 NameKind::Node | NameKind::Substitution => {
297 if chars[0].is_ascii_digit() {
299 return Err(Ros2ArgsError::InvalidName {
300 kind,
301 name: name.to_string(),
302 reason: "name must not start with a numeric character".to_string(),
303 });
304 }
305 if !is_valid_name_char(chars[0]) {
306 return Err(Ros2ArgsError::InvalidName {
307 kind,
308 name: name.to_string(),
309 reason: format!("invalid character '{}' at position 0", chars[0]),
310 });
311 }
312 }
313 }
314
315 let mut brace_depth = 0;
317 let mut prev_char: Option<char> = if i > 0 { Some(chars[i - 1]) } else { None };
318
319 while i < chars.len() {
320 let c = chars[i];
321
322 match kind {
323 NameKind::Topic => {
324 if c == '{' {
325 brace_depth += 1;
326 } else if c == '}' {
327 if brace_depth == 0 {
328 return Err(Ros2ArgsError::InvalidName {
329 kind,
330 name: name.to_string(),
331 reason: "unbalanced curly braces: unexpected '}'".to_string(),
332 });
333 }
334 brace_depth -= 1;
335 } else if brace_depth > 0 {
336 if !is_valid_name_char(c) {
338 return Err(Ros2ArgsError::InvalidName {
339 kind,
340 name: name.to_string(),
341 reason: format!(
342 "invalid character '{c}' inside substitution at position {i}"
343 ),
344 });
345 }
346 } else if !is_valid_topic_char(c) {
347 return Err(Ros2ArgsError::InvalidName {
348 kind,
349 name: name.to_string(),
350 reason: format!("invalid character '{c}' at position {i}"),
351 });
352 }
353
354 if c == '/' && prev_char == Some('/') {
356 return Err(Ros2ArgsError::InvalidName {
357 kind,
358 name: name.to_string(),
359 reason: "name must not contain repeated forward slashes (//)".to_string(),
360 });
361 }
362
363 if c == '_' && prev_char == Some('_') {
365 return Err(Ros2ArgsError::InvalidName {
366 kind,
367 name: name.to_string(),
368 reason: "name must not contain repeated underscores (__)".to_string(),
369 });
370 }
371
372 if c == '~' {
374 return Err(Ros2ArgsError::InvalidName {
375 kind,
376 name: name.to_string(),
377 reason: "tilde (~) may only appear at the beginning of a name".to_string(),
378 });
379 }
380
381 if prev_char == Some('/') && c.is_ascii_digit() {
383 return Err(Ros2ArgsError::InvalidName {
384 kind,
385 name: name.to_string(),
386 reason: format!(
387 "token after '/' must not start with a numeric character at position {i}"
388 ),
389 });
390 }
391 }
392 NameKind::Namespace => {
393 if !is_valid_topic_char(c) {
394 return Err(Ros2ArgsError::InvalidName {
395 kind,
396 name: name.to_string(),
397 reason: format!("invalid character '{c}' at position {i}"),
398 });
399 }
400
401 if c == '/' && prev_char == Some('/') {
403 return Err(Ros2ArgsError::InvalidName {
404 kind,
405 name: name.to_string(),
406 reason: "namespace must not contain repeated forward slashes (//)"
407 .to_string(),
408 });
409 }
410
411 if c == '_' && prev_char == Some('_') {
413 return Err(Ros2ArgsError::InvalidName {
414 kind,
415 name: name.to_string(),
416 reason: "namespace must not contain repeated underscores (__)".to_string(),
417 });
418 }
419
420 if prev_char == Some('/') && c.is_ascii_digit() {
422 return Err(Ros2ArgsError::InvalidName {
423 kind,
424 name: name.to_string(),
425 reason: format!(
426 "namespace token after '/' must not start with a numeric character at position {i}"
427 ),
428 });
429 }
430 }
431 NameKind::Node => {
432 if c == '/' {
434 return Err(Ros2ArgsError::InvalidName {
435 kind,
436 name: name.to_string(),
437 reason: "node name must not contain forward slash (/)".to_string(),
438 });
439 }
440 if c == '~' {
441 return Err(Ros2ArgsError::InvalidName {
442 kind,
443 name: name.to_string(),
444 reason: "node name must not contain tilde (~)".to_string(),
445 });
446 }
447 if c == '{' || c == '}' {
448 return Err(Ros2ArgsError::InvalidName {
449 kind,
450 name: name.to_string(),
451 reason: "node name must not contain curly braces".to_string(),
452 });
453 }
454 if !is_valid_name_char(c) {
455 return Err(Ros2ArgsError::InvalidName {
456 kind,
457 name: name.to_string(),
458 reason: format!("invalid character '{c}' at position {i}"),
459 });
460 }
461
462 if c == '_' && prev_char == Some('_') {
464 return Err(Ros2ArgsError::InvalidName {
465 kind,
466 name: name.to_string(),
467 reason: "node name must not contain repeated underscores (__)".to_string(),
468 });
469 }
470 }
471 NameKind::Substitution => {
472 if !is_valid_name_char(c) {
473 return Err(Ros2ArgsError::InvalidName {
474 kind,
475 name: name.to_string(),
476 reason: format!("invalid character '{c}' at position {i}"),
477 });
478 }
479 }
480 }
481
482 prev_char = Some(c);
483 i += 1;
484 }
485
486 match kind {
488 NameKind::Topic => {
489 if brace_depth != 0 {
491 return Err(Ros2ArgsError::InvalidName {
492 kind,
493 name: name.to_string(),
494 reason: "unbalanced curly braces: missing '}'".to_string(),
495 });
496 }
497
498 if name.ends_with('/') {
500 return Err(Ros2ArgsError::InvalidName {
501 kind,
502 name: name.to_string(),
503 reason: "name must not end with a forward slash (/)".to_string(),
504 });
505 }
506 }
507 NameKind::Namespace => {
508 if name.len() > 1 && name.ends_with('/') {
510 return Err(Ros2ArgsError::InvalidName {
511 kind,
512 name: name.to_string(),
513 reason: "namespace must not end with a forward slash (/)".to_string(),
514 });
515 }
516 }
517 NameKind::Node | NameKind::Substitution => {}
518 }
519
520 Ok(())
521}
522
523pub fn validate_fully_qualified_name(name: &str) -> Ros2ArgsResult<()> {
546 if name.is_empty() {
547 return Err(Ros2ArgsError::InvalidName {
548 kind: NameKind::Topic,
549 name: name.to_string(),
550 reason: "fully qualified name must not be empty".to_string(),
551 });
552 }
553
554 if !name.starts_with('/') {
555 return Err(Ros2ArgsError::InvalidName {
556 kind: NameKind::Topic,
557 name: name.to_string(),
558 reason: "fully qualified name must start with a forward slash (/)".to_string(),
559 });
560 }
561
562 if name.contains('~') {
563 return Err(Ros2ArgsError::InvalidName {
564 kind: NameKind::Topic,
565 name: name.to_string(),
566 reason: "fully qualified name must not contain tilde (~)".to_string(),
567 });
568 }
569
570 if name.contains('{') || name.contains('}') {
571 return Err(Ros2ArgsError::InvalidName {
572 kind: NameKind::Topic,
573 name: name.to_string(),
574 reason: "fully qualified name must not contain curly braces ({})".to_string(),
575 });
576 }
577
578 validate_topic_name(name)
580}
581
582#[inline]
584#[must_use]
585pub fn is_relative_name(name: &str) -> bool {
586 !name.is_empty() && !name.starts_with('/') && !name.starts_with('~')
587}
588
589#[inline]
591#[must_use]
592pub fn is_absolute_name(name: &str) -> bool {
593 name.starts_with('/')
594}
595
596#[inline]
598#[must_use]
599pub fn is_private_name(name: &str) -> bool {
600 name.starts_with('~')
601}
602
603#[must_use]
608pub fn is_hidden_name(name: &str) -> bool {
609 name.starts_with('_')
611 || name.contains("/_")
612 || name
613 .split('/')
614 .any(|token| !token.is_empty() && token.starts_with('_'))
615}
616
617pub fn expand_topic_name(
674 node_namespace: &str,
675 node_name: &str,
676 topic_name: &str,
677) -> Ros2ArgsResult<String> {
678 validate_namespace(node_namespace)?;
680 validate_node_name(node_name)?;
681 validate_topic_name(topic_name)?;
682
683 let expanded = if is_absolute_name(topic_name) {
684 topic_name.to_string()
686 } else if is_private_name(topic_name) {
687 let node_fqn = build_node_fqn(node_namespace, node_name);
689 if topic_name == "~" {
690 node_fqn
691 } else {
692 format!("{}{}", node_fqn, &topic_name[1..])
694 }
695 } else {
696 if node_namespace == "/" {
698 format!("/{topic_name}")
699 } else {
700 format!("{node_namespace}/{topic_name}")
701 }
702 };
703
704 validate_fully_qualified_name(&expanded)?;
706
707 Ok(expanded)
708}
709
710#[must_use]
722pub fn build_node_fqn(namespace: &str, node_name: &str) -> String {
723 if namespace == "/" {
724 format!("/{node_name}")
725 } else {
726 format!("{namespace}/{node_name}")
727 }
728}
729
730pub fn expand_topic_name_with_fqn(node_fqn: &str, topic_name: &str) -> Ros2ArgsResult<String> {
763 validate_fully_qualified_name(node_fqn)?;
765 validate_topic_name(topic_name)?;
766
767 let node_namespace = extract_namespace(node_fqn);
769
770 let expanded = if is_absolute_name(topic_name) {
771 topic_name.to_string()
773 } else if is_private_name(topic_name) {
774 if topic_name == "~" {
776 node_fqn.to_string()
777 } else {
778 format!("{node_fqn}{}", &topic_name[1..])
780 }
781 } else {
782 if node_namespace == "/" {
784 format!("/{topic_name}")
785 } else {
786 format!("{node_namespace}/{topic_name}")
787 }
788 };
789
790 validate_fully_qualified_name(&expanded)?;
792
793 Ok(expanded)
794}
795
796#[must_use]
810pub fn extract_namespace(node_fqn: &str) -> &str {
811 if let Some(last_slash_pos) = node_fqn.rfind('/') {
812 if last_slash_pos == 0 {
813 "/"
814 } else {
815 &node_fqn[..last_slash_pos]
816 }
817 } else {
818 "/"
819 }
820}
821
822#[must_use]
836pub fn extract_base_name(node_fqn: &str) -> &str {
837 if let Some(last_slash_pos) = node_fqn.rfind('/') {
838 &node_fqn[last_slash_pos + 1..]
839 } else {
840 node_fqn
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
851 fn test_valid_topic_names() {
852 let valid_names = [
853 "foo",
854 "bar",
855 "abc123",
856 "_foo",
857 "Foo",
858 "BAR",
859 "foo/bar",
860 "/foo",
861 "/foo/bar",
862 "~",
863 "~/foo",
864 "~/foo/bar",
865 "{foo}_bar",
866 "foo/{ping}/bar",
867 "foo/_bar",
868 "foo_/bar",
869 "foo_",
870 ];
871
872 for name in &valid_names {
873 assert!(
874 validate_topic_name(name).is_ok(),
875 "Expected '{name}' to be valid",
876 );
877 }
878 }
879
880 #[test]
881 fn test_invalid_topic_names_empty() {
882 assert!(validate_topic_name("").is_err());
883 }
884
885 #[test]
886 fn test_invalid_topic_names_start_with_number() {
887 assert!(validate_topic_name("123abc").is_err());
888 assert!(validate_topic_name("123").is_err());
889 }
890
891 #[test]
892 fn test_invalid_topic_names_double_slash() {
893 assert!(validate_topic_name("foo//bar").is_err());
894 assert!(validate_topic_name("//foo").is_err());
895 }
896
897 #[test]
898 fn test_invalid_topic_names_double_underscore() {
899 assert!(validate_topic_name("foo__bar").is_err());
900 }
901
902 #[test]
903 fn test_invalid_topic_names_tilde_not_at_start() {
904 assert!(validate_topic_name("/~").is_err());
905 assert!(validate_topic_name("foo~").is_err());
906 assert!(validate_topic_name("foo~/bar").is_err());
907 assert!(validate_topic_name("foo/~bar").is_err());
908 assert!(validate_topic_name("foo/~/bar").is_err());
909 }
910
911 #[test]
912 fn test_invalid_topic_names_tilde_not_followed_by_slash() {
913 assert!(validate_topic_name("~foo").is_err());
914 }
915
916 #[test]
917 fn test_invalid_topic_names_trailing_slash() {
918 assert!(validate_topic_name("foo/").is_err());
919 assert!(validate_topic_name("/foo/bar/").is_err());
920 }
921
922 #[test]
923 fn test_invalid_topic_names_space() {
924 assert!(validate_topic_name("foo bar").is_err());
925 assert!(validate_topic_name(" ").is_err());
926 }
927
928 #[test]
929 fn test_invalid_topic_names_unbalanced_braces() {
930 assert!(validate_topic_name("{foo").is_err());
931 assert!(validate_topic_name("foo}").is_err());
932 assert!(validate_topic_name("{foo/bar").is_err());
933 }
934
935 #[test]
938 fn test_valid_node_names() {
939 let valid_names = [
940 "my_node",
941 "node123",
942 "MyNode",
943 "NODE",
944 "_private_node",
945 "node_",
946 "a",
947 "A",
948 ];
949
950 for name in &valid_names {
951 assert!(
952 validate_node_name(name).is_ok(),
953 "Expected '{name}' to be valid node name",
954 );
955 }
956 }
957
958 #[test]
959 fn test_invalid_node_names_empty() {
960 assert!(validate_node_name("").is_err());
961 }
962
963 #[test]
964 fn test_invalid_node_names_start_with_number() {
965 assert!(validate_node_name("123node").is_err());
966 assert!(validate_node_name("1").is_err());
967 }
968
969 #[test]
970 fn test_invalid_node_names_contains_slash() {
971 assert!(validate_node_name("my/node").is_err());
972 assert!(validate_node_name("/node").is_err());
973 assert!(validate_node_name("node/").is_err());
974 }
975
976 #[test]
977 fn test_invalid_node_names_contains_tilde() {
978 assert!(validate_node_name("~node").is_err());
979 assert!(validate_node_name("node~").is_err());
980 assert!(validate_node_name("my~node").is_err());
981 }
982
983 #[test]
984 fn test_invalid_node_names_contains_braces() {
985 assert!(validate_node_name("{node}").is_err());
986 assert!(validate_node_name("node{").is_err());
987 assert!(validate_node_name("}node").is_err());
988 }
989
990 #[test]
991 fn test_invalid_node_names_double_underscore() {
992 assert!(validate_node_name("my__node").is_err());
993 }
994
995 #[test]
996 fn test_invalid_node_names_special_chars() {
997 assert!(validate_node_name("my-node").is_err());
998 assert!(validate_node_name("my.node").is_err());
999 assert!(validate_node_name("my node").is_err());
1000 assert!(validate_node_name("my@node").is_err());
1001 }
1002
1003 #[test]
1006 fn test_valid_namespaces() {
1007 let valid_namespaces = [
1008 "/",
1009 "/foo",
1010 "/foo/bar",
1011 "/foo/bar/baz",
1012 "/my_namespace",
1013 "/_private",
1014 ];
1015
1016 for ns in &valid_namespaces {
1017 assert!(
1018 validate_namespace(ns).is_ok(),
1019 "Expected '{ns}' to be valid namespace",
1020 );
1021 }
1022 }
1023
1024 #[test]
1025 fn test_invalid_namespace_empty() {
1026 assert!(validate_namespace("").is_err());
1027 }
1028
1029 #[test]
1030 fn test_invalid_namespace_not_starting_with_slash() {
1031 assert!(validate_namespace("foo").is_err());
1032 assert!(validate_namespace("foo/bar").is_err());
1033 }
1034
1035 #[test]
1036 fn test_invalid_namespace_trailing_slash() {
1037 assert!(validate_namespace("/foo/").is_err());
1038 assert!(validate_namespace("/foo/bar/").is_err());
1039 }
1040
1041 #[test]
1042 fn test_invalid_namespace_double_slash() {
1043 assert!(validate_namespace("//foo").is_err());
1044 assert!(validate_namespace("/foo//bar").is_err());
1045 }
1046
1047 #[test]
1048 fn test_invalid_namespace_double_underscore() {
1049 assert!(validate_namespace("/foo__bar").is_err());
1050 }
1051
1052 #[test]
1053 fn test_invalid_namespace_token_starts_with_number() {
1054 assert!(validate_namespace("/123").is_err());
1055 assert!(validate_namespace("/foo/123bar").is_err());
1056 }
1057
1058 #[test]
1061 fn test_valid_fully_qualified_names() {
1062 let valid_names = [
1063 "/foo",
1064 "/bar/baz",
1065 "/_private/thing",
1066 "/public_namespace/_private/thing",
1067 ];
1068
1069 for name in &valid_names {
1070 assert!(
1071 validate_fully_qualified_name(name).is_ok(),
1072 "Expected '{name}' to be valid FQN",
1073 );
1074 }
1075 }
1076
1077 #[test]
1078 fn test_invalid_fqn_not_absolute() {
1079 assert!(validate_fully_qualified_name("foo").is_err());
1080 assert!(validate_fully_qualified_name("foo/bar").is_err());
1081 }
1082
1083 #[test]
1084 fn test_invalid_fqn_contains_tilde() {
1085 assert!(validate_fully_qualified_name("/~").is_err());
1086 assert!(validate_fully_qualified_name("/~/foo").is_err());
1087 }
1088
1089 #[test]
1090 fn test_invalid_fqn_contains_substitution() {
1091 assert!(validate_fully_qualified_name("/{sub}").is_err());
1092 assert!(validate_fully_qualified_name("/foo/{bar}").is_err());
1093 }
1094
1095 #[test]
1098 fn test_is_relative_name() {
1099 assert!(is_relative_name("foo"));
1100 assert!(is_relative_name("foo/bar"));
1101 assert!(!is_relative_name("/foo"));
1102 assert!(!is_relative_name("~"));
1103 assert!(!is_relative_name("~/foo"));
1104 assert!(!is_relative_name(""));
1105 }
1106
1107 #[test]
1108 fn test_is_absolute_name() {
1109 assert!(is_absolute_name("/foo"));
1110 assert!(is_absolute_name("/"));
1111 assert!(!is_absolute_name("foo"));
1112 assert!(!is_absolute_name("~"));
1113 }
1114
1115 #[test]
1116 fn test_is_private_name() {
1117 assert!(is_private_name("~"));
1118 assert!(is_private_name("~/foo"));
1119 assert!(!is_private_name("/foo"));
1120 assert!(!is_private_name("foo"));
1121 }
1122
1123 #[test]
1124 fn test_is_hidden_name() {
1125 assert!(is_hidden_name("_foo"));
1126 assert!(is_hidden_name("/foo/_bar"));
1127 assert!(is_hidden_name("/_private/thing"));
1128 assert!(!is_hidden_name("foo"));
1129 assert!(!is_hidden_name("/foo/bar"));
1130 assert!(!is_hidden_name("foo_bar"));
1131 }
1132
1133 #[test]
1134 fn test_is_valid_name_char() {
1135 assert!(is_valid_name_char('a'));
1136 assert!(is_valid_name_char('Z'));
1137 assert!(is_valid_name_char('5'));
1138 assert!(is_valid_name_char('_'));
1139 assert!(!is_valid_name_char('/'));
1140 assert!(!is_valid_name_char('-'));
1141 assert!(!is_valid_name_char(' '));
1142 }
1143
1144 #[test]
1145 fn test_is_valid_topic_char() {
1146 assert!(is_valid_topic_char('a'));
1147 assert!(is_valid_topic_char('Z'));
1148 assert!(is_valid_topic_char('5'));
1149 assert!(is_valid_topic_char('_'));
1150 assert!(is_valid_topic_char('/'));
1151 assert!(!is_valid_topic_char('-'));
1152 assert!(!is_valid_topic_char(' '));
1153 }
1154
1155 #[test]
1158 fn test_valid_substitutions() {
1159 assert!(validate_substitution("node").is_ok());
1160 assert!(validate_substitution("namespace").is_ok());
1161 assert!(validate_substitution("foo_bar").is_ok());
1162 assert!(validate_substitution("_private").is_ok());
1163 }
1164
1165 #[test]
1166 fn test_invalid_substitution_empty() {
1167 assert!(validate_substitution("").is_err());
1168 }
1169
1170 #[test]
1171 fn test_invalid_substitution_starts_with_number() {
1172 assert!(validate_substitution("123").is_err());
1173 assert!(validate_substitution("1foo").is_err());
1174 }
1175
1176 #[test]
1177 fn test_invalid_substitution_special_chars() {
1178 assert!(validate_substitution("foo/bar").is_err());
1179 assert!(validate_substitution("foo-bar").is_err());
1180 }
1181
1182 #[test]
1185 fn test_expand_absolute_topic() {
1186 let fqn = expand_topic_name("/my_ns", "my_node", "/absolute/topic").unwrap();
1188 assert_eq!(fqn, "/absolute/topic");
1189
1190 let fqn = expand_topic_name("/", "node", "/foo").unwrap();
1191 assert_eq!(fqn, "/foo");
1192
1193 let fqn = expand_topic_name("/deep/ns", "node", "/other/topic").unwrap();
1194 assert_eq!(fqn, "/other/topic");
1195 }
1196
1197 #[test]
1198 fn test_expand_private_topic() {
1199 let fqn = expand_topic_name("/my_ns", "my_node", "~/private").unwrap();
1201 assert_eq!(fqn, "/my_ns/my_node/private");
1202
1203 let fqn = expand_topic_name("/my_ns", "my_node", "~").unwrap();
1204 assert_eq!(fqn, "/my_ns/my_node");
1205
1206 let fqn = expand_topic_name("/", "my_node", "~/private").unwrap();
1207 assert_eq!(fqn, "/my_node/private");
1208
1209 let fqn = expand_topic_name("/", "my_node", "~").unwrap();
1210 assert_eq!(fqn, "/my_node");
1211
1212 let fqn = expand_topic_name("/foo/bar", "node", "~/baz").unwrap();
1213 assert_eq!(fqn, "/foo/bar/node/baz");
1214 }
1215
1216 #[test]
1217 fn test_expand_relative_topic() {
1218 let fqn = expand_topic_name("/my_ns", "my_node", "relative").unwrap();
1220 assert_eq!(fqn, "/my_ns/relative");
1221
1222 let fqn = expand_topic_name("/my_ns", "my_node", "foo/bar").unwrap();
1223 assert_eq!(fqn, "/my_ns/foo/bar");
1224
1225 let fqn = expand_topic_name("/", "my_node", "relative").unwrap();
1226 assert_eq!(fqn, "/relative");
1227
1228 let fqn = expand_topic_name("/deep/namespace", "node", "topic").unwrap();
1229 assert_eq!(fqn, "/deep/namespace/topic");
1230 }
1231
1232 #[test]
1233 fn test_expand_topic_with_fqn() {
1234 let fqn = expand_topic_name_with_fqn("/my_ns/my_node", "~/private").unwrap();
1235 assert_eq!(fqn, "/my_ns/my_node/private");
1236
1237 let fqn = expand_topic_name_with_fqn("/my_ns/my_node", "/absolute").unwrap();
1238 assert_eq!(fqn, "/absolute");
1239
1240 let fqn = expand_topic_name_with_fqn("/my_ns/my_node", "relative").unwrap();
1241 assert_eq!(fqn, "/my_ns/relative");
1242
1243 let fqn = expand_topic_name_with_fqn("/my_node", "~/private").unwrap();
1244 assert_eq!(fqn, "/my_node/private");
1245
1246 let fqn = expand_topic_name_with_fqn("/my_node", "relative").unwrap();
1247 assert_eq!(fqn, "/relative");
1248 }
1249
1250 #[test]
1251 fn test_build_node_fqn() {
1252 assert_eq!(build_node_fqn("/my_ns", "my_node"), "/my_ns/my_node");
1253 assert_eq!(build_node_fqn("/", "my_node"), "/my_node");
1254 assert_eq!(build_node_fqn("/foo/bar", "node"), "/foo/bar/node");
1255 }
1256
1257 #[test]
1258 fn test_extract_namespace() {
1259 assert_eq!(extract_namespace("/my_ns/my_node"), "/my_ns");
1260 assert_eq!(extract_namespace("/my_node"), "/");
1261 assert_eq!(extract_namespace("/foo/bar/baz"), "/foo/bar");
1262 assert_eq!(extract_namespace("/a/b/c/d"), "/a/b/c");
1263 }
1264
1265 #[test]
1266 fn test_extract_base_name() {
1267 assert_eq!(extract_base_name("/my_ns/my_node"), "my_node");
1268 assert_eq!(extract_base_name("/my_node"), "my_node");
1269 assert_eq!(extract_base_name("/foo/bar/baz"), "baz");
1270 }
1271
1272 #[test]
1273 fn test_expand_topic_invalid_inputs() {
1274 assert!(expand_topic_name("invalid", "node", "topic").is_err());
1276
1277 assert!(expand_topic_name("/ns", "invalid/node", "topic").is_err());
1279
1280 assert!(expand_topic_name("/ns", "node", "invalid//topic").is_err());
1282 }
1283}