use std::str::FromStr;
use re_types_core::ComponentName;
use crate::{ComponentPath, DataPath, EntityPath, EntityPathPart, Instance};
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum PathParseError {
#[error("Expected path, found empty string")]
EmptyString,
#[error("No entity path found")]
MissingPath,
#[error("Double-slashes with no part between")]
DoubleSlash,
#[error("Missing slash (/)")]
MissingSlash,
#[error("Extra trailing slash (/)")]
TrailingSlash,
#[error("Empty part")]
EmptyPart,
#[error("Invalid instance index: {0:?} (expected '[#1234]')")]
BadInstance(String),
#[error("Found an unexpected instance index: [#{}]", 0)]
UnexpectedInstance(Instance),
#[error("Found an unexpected trailing component name: {0:?}")]
UnexpectedComponentName(ComponentName),
#[error("Found no component name")]
MissingComponentName,
#[error("Found trailing colon (:)")]
TrailingColon,
#[error("Unknown escape sequence: \\{0}")]
UnknownEscapeSequence(char),
#[error("String ended in backslash")]
TrailingBackslash,
#[error("{0:?} needs to be escaped as `\\{0}`")]
MissingEscape(char),
#[error("Expected e.g. '\\u{{262E}}', found: '\\u{0}'")]
InvalidUnicodeEscape(String),
}
type Result<T, E = PathParseError> = std::result::Result<T, E>;
impl std::str::FromStr for DataPath {
type Err = PathParseError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
if path.is_empty() {
return Err(PathParseError::EmptyString);
}
let mut tokens = tokenize_data_path(path);
let mut component_name = None;
let mut instance = None;
if let Some(colon) = tokens.iter().position(|&token| token == ":") {
let component_tokens = &tokens[colon + 1..];
if component_tokens.is_empty() {
return Err(PathParseError::TrailingColon);
} else {
let mut name = join(component_tokens);
if !name.contains('.') {
name = format!("rerun.components.{name}");
}
component_name = Some(ComponentName::from(name));
}
tokens.truncate(colon);
}
if let Some(bracket) = tokens.iter().position(|&token| token == "[") {
let instance_tokens = &tokens[bracket..];
if instance_tokens.len() != 3 || instance_tokens.last() != Some(&"]") {
return Err(PathParseError::BadInstance(join(instance_tokens)));
}
let instance_token = instance_tokens[1];
if let Some(nr) = instance_token.strip_prefix('#') {
if let Ok(nr) = u64::from_str(nr) {
instance = Some(nr);
} else {
return Err(PathParseError::BadInstance(instance_token.to_owned()));
}
} else {
return Err(PathParseError::BadInstance(instance_token.to_owned()));
}
tokens.truncate(bracket);
}
let parts = entity_path_parts_from_tokens_strict(&tokens)?;
let entity_path = EntityPath::from(parts);
Ok(Self {
entity_path,
instance: instance.map(Into::into),
component_name,
})
}
}
impl EntityPath {
pub fn parse_strict(input: &str) -> Result<Self, PathParseError> {
let DataPath {
entity_path,
instance,
component_name,
} = DataPath::from_str(input)?;
if let Some(instance) = instance {
return Err(PathParseError::UnexpectedInstance(instance));
}
if let Some(component_name) = component_name {
return Err(PathParseError::UnexpectedComponentName(component_name));
}
Ok(entity_path)
}
pub fn parse_forgiving(input: &str) -> Self {
let mut warnings = vec![];
let parts: Vec<_> = tokenize_entity_path(input)
.into_iter()
.filter(|&part| part != "/") .map(|part| EntityPathPart::parse_forgiving_with_warning(part, Some(&mut warnings)))
.collect();
let path = Self::from(parts);
if let Some(warning) = warnings.first() {
re_log::warn_once!("When parsing the entity path {input:?}: {warning}. The path will be interpreted as {path}");
}
path
}
}
impl FromStr for ComponentPath {
type Err = PathParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let DataPath {
entity_path,
instance,
component_name,
} = DataPath::from_str(s)?;
if let Some(instance) = instance {
return Err(PathParseError::UnexpectedInstance(instance));
}
let Some(component_name) = component_name else {
return Err(PathParseError::MissingComponentName);
};
Ok(Self {
entity_path,
component_name,
})
}
}
fn entity_path_parts_from_tokens_strict(mut tokens: &[&str]) -> Result<Vec<EntityPathPart>> {
if tokens.is_empty() {
return Err(PathParseError::MissingPath);
}
if tokens == ["/"] {
return Ok(vec![]); }
if tokens[0] == "/" {
tokens = &tokens[1..];
}
let mut parts = vec![];
loop {
let token = tokens[0];
tokens = &tokens[1..];
if token == "/" {
return Err(PathParseError::DoubleSlash);
} else {
parts.push(EntityPathPart::parse_strict(token)?);
}
if let Some(next_token) = tokens.first() {
if *next_token == "/" {
tokens = &tokens[1..];
if tokens.is_empty() {
return Err(PathParseError::TrailingSlash);
}
} else {
return Err(PathParseError::MissingSlash);
}
} else {
break;
}
}
Ok(parts)
}
fn join(tokens: &[&str]) -> String {
let mut s = String::default();
for token in tokens {
s.push_str(token);
}
s
}
fn tokenize_entity_path(path: &str) -> Vec<&str> {
tokenize_by(path, b"/")
}
fn tokenize_data_path(path: &str) -> Vec<&str> {
tokenize_by(path, b"/[]:")
}
fn tokenize_by<'s>(path: &'s str, special_chars: &[u8]) -> Vec<&'s str> {
#![allow(clippy::unwrap_used)]
let mut bytes = path.as_bytes();
let mut tokens = vec![];
while !bytes.is_empty() {
let mut i = 0;
let mut is_in_escape = false;
while i < bytes.len() {
if !is_in_escape && special_chars.contains(&bytes[i]) {
break;
}
is_in_escape = bytes[i] == b'\\';
i += 1;
}
if i == 0 {
i = 1;
}
tokens.push(&bytes[..i]);
bytes = &bytes[i..];
}
tokens
.iter()
.map(|token| std::str::from_utf8(token).unwrap())
.collect()
}
#[test]
fn test_parse_entity_path_forgiving() {
use crate::entity_path_vec;
fn parse(s: &str) -> Vec<EntityPathPart> {
EntityPath::parse_forgiving(s).to_vec()
}
fn normalize(s: &str) -> String {
EntityPath::parse_forgiving(s).to_string()
}
assert_eq!(parse(""), entity_path_vec!());
assert_eq!(parse("/"), entity_path_vec!());
assert_eq!(parse("foo"), entity_path_vec!("foo"));
assert_eq!(parse("foo/bar"), entity_path_vec!("foo", "bar"));
assert_eq!(
parse(r#"foo/bar :/."#),
entity_path_vec!("foo", "bar :", ".",)
);
assert_eq!(parse("hallådär"), entity_path_vec!("hallådär"));
assert_eq!(normalize(""), "/");
assert_eq!(normalize("/"), "/");
assert_eq!(normalize("//"), "/");
assert_eq!(normalize("/foo/bar/"), "/foo/bar");
assert_eq!(normalize("/foo///bar//"), "/foo/bar");
assert_eq!(normalize("foo/bar:baz"), r#"/foo/bar\:baz"#);
assert_eq!(normalize("foo/42"), "/foo/42");
assert_eq!(normalize("foo/#bar/baz"), r##"/foo/\#bar/baz"##);
assert_eq!(normalize("foo/Hallå Där!"), r#"/foo/Hallå\ Där\!"#);
}
#[test]
fn test_parse_entity_path_strict() {
use crate::entity_path_vec;
fn parse(s: &str) -> Result<Vec<EntityPathPart>> {
EntityPath::parse_strict(s).map(|path| path.to_vec())
}
assert_eq!(parse(""), Err(PathParseError::EmptyString));
assert_eq!(parse("/"), Ok(entity_path_vec!()));
assert_eq!(parse("foo"), Ok(entity_path_vec!("foo")));
assert_eq!(parse("/foo"), Ok(entity_path_vec!("foo")));
assert_eq!(parse("foo/bar"), Ok(entity_path_vec!("foo", "bar")));
assert_eq!(parse("/foo/bar"), Ok(entity_path_vec!("foo", "bar")));
assert_eq!(parse("foo//bar"), Err(PathParseError::DoubleSlash));
assert_eq!(parse("foo/bar/"), Err(PathParseError::TrailingSlash));
assert!(matches!(
parse(r#"entity:component"#),
Err(PathParseError::UnexpectedComponentName { .. })
));
assert!(matches!(
parse(r#"entity[#123]"#),
Err(PathParseError::UnexpectedInstance(Instance(123)))
));
assert_eq!(parse("hallådär"), Ok(entity_path_vec!("hallådär")));
}
#[test]
fn test_parse_component_path() {
assert_eq!(
ComponentPath::from_str("world/points:rerun.components.Color"),
Ok(ComponentPath {
entity_path: EntityPath::from("world/points"),
component_name: "rerun.components.Color".into(),
})
);
assert_eq!(
ComponentPath::from_str("world/points:Color"),
Ok(ComponentPath {
entity_path: EntityPath::from("world/points"),
component_name: "rerun.components.Color".into(),
})
);
assert_eq!(
ComponentPath::from_str("world/points:my.custom.color"),
Ok(ComponentPath {
entity_path: EntityPath::from("world/points"),
component_name: "my.custom.color".into(),
})
);
assert_eq!(
ComponentPath::from_str("world/points:"),
Err(PathParseError::TrailingColon)
);
assert_eq!(
ComponentPath::from_str("world/points"),
Err(PathParseError::MissingComponentName)
);
assert_eq!(
ComponentPath::from_str("world/points[#42]:rerun.components.Color"),
Err(PathParseError::UnexpectedInstance(Instance(42)))
);
}
#[test]
fn test_parse_data_path() {
assert_eq!(
DataPath::from_str("world/points[#42]:rerun.components.Color"),
Ok(DataPath {
entity_path: EntityPath::from("world/points"),
instance: Some(Instance(42)),
component_name: Some("rerun.components.Color".into()),
})
);
assert_eq!(
DataPath::from_str("world/points:rerun.components.Color"),
Ok(DataPath {
entity_path: EntityPath::from("world/points"),
instance: None,
component_name: Some("rerun.components.Color".into()),
})
);
assert_eq!(
DataPath::from_str("world/points[#42]"),
Ok(DataPath {
entity_path: EntityPath::from("world/points"),
instance: Some(Instance(42)),
component_name: None,
})
);
assert_eq!(
DataPath::from_str("world/points"),
Ok(DataPath {
entity_path: EntityPath::from("world/points"),
instance: None,
component_name: None,
})
);
assert!(matches!(
DataPath::from_str(r#"hello there"#),
Err(PathParseError::MissingEscape(' '))
));
assert!(DataPath::from_str(r#"hello_there"#).is_ok());
assert!(DataPath::from_str(r#"hello-there"#).is_ok());
assert!(DataPath::from_str(r#"hello.there"#).is_ok());
assert!(DataPath::from_str(r#"hallådär"#).is_ok());
}